@farcaster/snap 1.5.1 → 2.0.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/constants.d.ts +0 -107
- package/dist/constants.js +0 -148
- package/dist/dataStore.d.ts +12 -0
- package/dist/dataStore.js +35 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -3
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.js +3 -0
- package/dist/react/accent-context.d.ts +6 -0
- package/dist/react/accent-context.js +10 -0
- package/dist/react/catalog-renderer.d.ts +5 -0
- package/dist/react/catalog-renderer.js +37 -0
- package/dist/react/components/action-button.d.ts +6 -0
- package/dist/react/components/action-button.js +22 -0
- package/dist/react/components/badge.d.ts +5 -0
- package/dist/react/components/badge.js +18 -0
- package/dist/react/components/icon.d.ts +7 -0
- package/dist/react/components/icon.js +60 -0
- package/dist/react/components/image.d.ts +5 -0
- package/dist/react/components/image.js +15 -0
- package/dist/react/components/input.d.ts +5 -0
- package/dist/react/components/input.js +18 -0
- package/dist/react/components/item-group.d.ts +7 -0
- package/dist/react/components/item-group.js +17 -0
- package/dist/react/components/item.d.ts +7 -0
- package/dist/react/components/item.js +9 -0
- package/dist/react/components/progress.d.ts +5 -0
- package/dist/react/components/progress.js +11 -0
- package/dist/react/components/separator.d.ts +5 -0
- package/dist/react/components/separator.js +7 -0
- package/dist/react/components/slider.d.ts +5 -0
- package/dist/react/components/slider.js +21 -0
- package/dist/react/components/stack.d.ts +7 -0
- package/dist/react/components/stack.js +32 -0
- package/dist/react/components/switch.d.ts +5 -0
- package/dist/react/components/switch.js +23 -0
- package/dist/react/components/text.d.ts +5 -0
- package/dist/react/components/text.js +25 -0
- package/dist/react/components/toggle-group.d.ts +5 -0
- package/dist/react/components/toggle-group.js +52 -0
- package/dist/react/hooks/use-snap-accent.d.ts +13 -0
- package/dist/react/hooks/use-snap-accent.js +32 -0
- package/dist/react/index.d.ts +47 -0
- package/dist/react/index.js +191 -0
- package/dist/react/lib/preview-primary-css.d.ts +6 -0
- package/dist/react/lib/preview-primary-css.js +43 -0
- package/dist/react/lib/resolve-palette-hex.d.ts +2 -0
- package/dist/react/lib/resolve-palette-hex.js +10 -0
- package/dist/schemas.d.ts +14 -1629
- package/dist/schemas.js +14 -526
- package/dist/ui/badge.d.ts +52 -0
- package/dist/ui/badge.js +9 -0
- package/dist/ui/button.d.ts +42 -28
- package/dist/ui/button.js +7 -9
- package/dist/ui/catalog.d.ts +280 -155
- package/dist/ui/catalog.js +102 -83
- package/dist/ui/icon.d.ts +56 -0
- package/dist/ui/icon.js +51 -0
- package/dist/ui/image.d.ts +1 -0
- package/dist/ui/image.js +2 -2
- package/dist/ui/index.d.ts +20 -22
- package/dist/ui/index.js +10 -11
- package/dist/ui/input.d.ts +17 -0
- package/dist/ui/input.js +13 -0
- package/dist/ui/item-group.d.ts +12 -0
- package/dist/ui/item-group.js +7 -0
- package/dist/ui/item.d.ts +14 -0
- package/dist/ui/item.js +9 -0
- package/dist/ui/progress.d.ts +1 -11
- package/dist/ui/progress.js +21 -4
- package/dist/ui/schema.js +3 -3
- package/dist/ui/separator.d.ts +9 -0
- package/dist/ui/separator.js +5 -0
- package/dist/ui/slider.d.ts +4 -3
- package/dist/ui/slider.js +34 -5
- package/dist/ui/stack.d.ts +22 -1
- package/dist/ui/stack.js +8 -1
- package/dist/ui/switch.d.ts +8 -0
- package/dist/ui/switch.js +7 -0
- package/dist/ui/text.d.ts +15 -7
- package/dist/ui/text.js +8 -4
- package/dist/ui/toggle-group.d.ts +23 -0
- package/dist/ui/toggle-group.js +19 -0
- package/dist/validator.d.ts +5 -1
- package/dist/validator.js +6 -136
- package/package.json +72 -52
- package/src/constants.ts +0 -179
- package/src/dataStore.ts +62 -0
- package/src/index.ts +11 -20
- package/src/middleware.ts +7 -0
- package/src/react/accent-context.tsx +29 -0
- package/src/react/catalog-renderer.tsx +39 -0
- package/src/react/components/action-button.tsx +48 -0
- package/src/react/components/badge.tsx +37 -0
- package/src/react/components/icon.tsx +115 -0
- package/src/react/components/image.tsx +33 -0
- package/src/react/components/input.tsx +36 -0
- package/src/react/components/item-group.tsx +43 -0
- package/src/react/components/item.tsx +33 -0
- package/src/react/components/progress.tsx +29 -0
- package/src/react/components/separator.tsx +14 -0
- package/src/react/components/slider.tsx +43 -0
- package/src/react/components/stack.tsx +55 -0
- package/src/react/components/switch.tsx +46 -0
- package/src/react/components/text.tsx +43 -0
- package/src/react/components/toggle-group.tsx +85 -0
- package/src/react/hooks/use-snap-accent.ts +45 -0
- package/src/react/index.tsx +321 -0
- package/src/react/lib/preview-primary-css.ts +57 -0
- package/src/react/lib/resolve-palette-hex.ts +20 -0
- package/src/schemas.ts +18 -644
- package/src/ui/badge.ts +13 -0
- package/src/ui/button.ts +9 -12
- package/src/ui/catalog.ts +106 -86
- package/src/ui/icon.ts +56 -0
- package/src/ui/image.ts +3 -2
- package/src/ui/index.ts +26 -29
- package/src/ui/input.ts +17 -0
- package/src/ui/item-group.ts +11 -0
- package/src/ui/item.ts +13 -0
- package/src/ui/progress.ts +25 -7
- package/src/ui/schema.ts +3 -3
- package/src/ui/separator.ts +9 -0
- package/src/ui/slider.ts +40 -10
- package/src/ui/stack.ts +9 -1
- package/src/ui/switch.ts +11 -0
- package/src/ui/text.ts +9 -4
- package/src/ui/toggle-group.ts +23 -0
- package/src/validator.ts +6 -176
- package/dist/ui/bar-chart.d.ts +0 -30
- package/dist/ui/bar-chart.js +0 -15
- package/dist/ui/button-group.d.ts +0 -19
- package/dist/ui/button-group.js +0 -18
- package/dist/ui/divider.d.ts +0 -3
- package/dist/ui/divider.js +0 -2
- package/dist/ui/grid.d.ts +0 -22
- package/dist/ui/grid.js +0 -16
- package/dist/ui/group.d.ts +0 -7
- package/dist/ui/group.js +0 -5
- package/dist/ui/list.d.ts +0 -13
- package/dist/ui/list.js +0 -13
- package/dist/ui/spacer.d.ts +0 -9
- package/dist/ui/spacer.js +0 -5
- package/dist/ui/text-input.d.ts +0 -7
- package/dist/ui/text-input.js +0 -12
- package/dist/ui/toggle.d.ts +0 -7
- package/dist/ui/toggle.js +0 -6
- package/src/ui/bar-chart.ts +0 -20
- package/src/ui/button-group.ts +0 -26
- package/src/ui/divider.ts +0 -5
- package/src/ui/grid.ts +0 -25
- package/src/ui/group.ts +0 -8
- package/src/ui/list.ts +0 -17
- package/src/ui/spacer.ts +0 -8
- package/src/ui/text-input.ts +0 -15
- package/src/ui/toggle.ts +0 -9
package/src/constants.ts
CHANGED
|
@@ -1,184 +1,5 @@
|
|
|
1
|
-
export const POST_GRID_TAP_KEY = "grid_tap" as const;
|
|
2
|
-
|
|
3
1
|
export const SPEC_VERSION = "1.0" as const;
|
|
4
2
|
|
|
5
3
|
export const MEDIA_TYPE = "application/vnd.farcaster.snap+json" as const;
|
|
6
4
|
|
|
7
|
-
export const LIMITS = {
|
|
8
|
-
maxElementsPerPage: 5,
|
|
9
|
-
maxButtonsPerPage: 4,
|
|
10
|
-
maxTextInputChars: 280,
|
|
11
|
-
maxListItems: 4,
|
|
12
|
-
minListItems: 1,
|
|
13
|
-
minButtonGroupOptions: 2,
|
|
14
|
-
maxButtonGroupOptions: 4,
|
|
15
|
-
maxButtonGroupOptionChars: 40,
|
|
16
|
-
maxButtonLabelChars: 30,
|
|
17
|
-
listItemContentMaxChars: 100,
|
|
18
|
-
listItemTrailingMaxChars: 40,
|
|
19
|
-
minGridCols: 2,
|
|
20
|
-
maxGridCols: 64,
|
|
21
|
-
minGridRows: 2,
|
|
22
|
-
maxGridRows: 8,
|
|
23
|
-
minGroupChildren: 2,
|
|
24
|
-
maxGroupChildren: 3,
|
|
25
|
-
maxBarChartBars: 6,
|
|
26
|
-
barChartLabelMaxChars: 40,
|
|
27
|
-
maxEstimatedPageHeightPx: 500,
|
|
28
|
-
} as const;
|
|
29
|
-
|
|
30
|
-
export const TEXT_STYLE = {
|
|
31
|
-
title: "title",
|
|
32
|
-
body: "body",
|
|
33
|
-
caption: "caption",
|
|
34
|
-
label: "label",
|
|
35
|
-
} as const;
|
|
36
|
-
|
|
37
|
-
export const TEXT_STYLE_VALUES = [
|
|
38
|
-
TEXT_STYLE.title,
|
|
39
|
-
TEXT_STYLE.body,
|
|
40
|
-
TEXT_STYLE.caption,
|
|
41
|
-
TEXT_STYLE.label,
|
|
42
|
-
] as const;
|
|
43
|
-
|
|
44
|
-
export const TEXT_ALIGN_VALUES = ["left", "center", "right"] as const;
|
|
45
|
-
export const IMAGE_ASPECT_VALUES = [
|
|
46
|
-
"1:1",
|
|
47
|
-
"16:9",
|
|
48
|
-
"4:3",
|
|
49
|
-
"3:4",
|
|
50
|
-
"9:16",
|
|
51
|
-
] as const;
|
|
52
|
-
|
|
53
|
-
export const SPACER_SIZE = {
|
|
54
|
-
small: "small",
|
|
55
|
-
medium: "medium",
|
|
56
|
-
large: "large",
|
|
57
|
-
} as const;
|
|
58
|
-
|
|
59
|
-
export const SPACER_SIZE_VALUES = [
|
|
60
|
-
SPACER_SIZE.small,
|
|
61
|
-
SPACER_SIZE.medium,
|
|
62
|
-
SPACER_SIZE.large,
|
|
63
|
-
] as const;
|
|
64
|
-
|
|
65
|
-
export const LIST_STYLE_VALUES = ["ordered", "unordered", "plain"] as const;
|
|
66
|
-
|
|
67
|
-
export const DEFAULT_LIST_STYLE = "ordered" as const;
|
|
68
|
-
|
|
69
|
-
export const GRID_CELL_SIZE_VALUES = ["auto", "square"] as const;
|
|
70
|
-
export const GRID_GAP_VALUES = ["none", "small", "medium"] as const;
|
|
71
|
-
export const DEFAULT_GRID_GAP =
|
|
72
|
-
"small" as const satisfies (typeof GRID_GAP_VALUES)[number];
|
|
73
|
-
|
|
74
|
-
export const BUTTON_GROUP_STYLE = {
|
|
75
|
-
row: "row",
|
|
76
|
-
stack: "stack",
|
|
77
|
-
grid: "grid",
|
|
78
|
-
} as const;
|
|
79
|
-
|
|
80
|
-
export const BUTTON_GROUP_STYLE_VALUES = [
|
|
81
|
-
BUTTON_GROUP_STYLE.row,
|
|
82
|
-
BUTTON_GROUP_STYLE.stack,
|
|
83
|
-
BUTTON_GROUP_STYLE.grid,
|
|
84
|
-
] as const;
|
|
85
|
-
|
|
86
|
-
export const BUTTON_ACTION = {
|
|
87
|
-
post: "post",
|
|
88
|
-
link: "link",
|
|
89
|
-
mini_app: "mini_app",
|
|
90
|
-
client: "client",
|
|
91
|
-
} as const;
|
|
92
|
-
|
|
93
|
-
export const BUTTON_ACTION_VALUES = [
|
|
94
|
-
BUTTON_ACTION.post,
|
|
95
|
-
BUTTON_ACTION.link,
|
|
96
|
-
BUTTON_ACTION.mini_app,
|
|
97
|
-
BUTTON_ACTION.client,
|
|
98
|
-
] as const;
|
|
99
|
-
|
|
100
|
-
export const CLIENT_ACTION = {
|
|
101
|
-
view_cast: "view_cast",
|
|
102
|
-
view_profile: "view_profile",
|
|
103
|
-
compose_cast: "compose_cast",
|
|
104
|
-
view_token: "view_token",
|
|
105
|
-
send_token: "send_token",
|
|
106
|
-
swap_token: "swap_token",
|
|
107
|
-
} as const;
|
|
108
|
-
|
|
109
|
-
export const CLIENT_ACTION_VALUES = [
|
|
110
|
-
CLIENT_ACTION.view_cast,
|
|
111
|
-
CLIENT_ACTION.view_profile,
|
|
112
|
-
CLIENT_ACTION.compose_cast,
|
|
113
|
-
CLIENT_ACTION.view_token,
|
|
114
|
-
CLIENT_ACTION.send_token,
|
|
115
|
-
CLIENT_ACTION.swap_token,
|
|
116
|
-
] as const;
|
|
117
|
-
|
|
118
|
-
export const BUTTON_STYLE = {
|
|
119
|
-
primary: "primary",
|
|
120
|
-
secondary: "secondary",
|
|
121
|
-
} as const;
|
|
122
|
-
|
|
123
|
-
export const BUTTON_STYLE_VALUES = [
|
|
124
|
-
BUTTON_STYLE.primary,
|
|
125
|
-
BUTTON_STYLE.secondary,
|
|
126
|
-
] as const;
|
|
127
|
-
|
|
128
|
-
export const BUTTON_LAYOUT_VALUES = ["stack", "row", "grid"] as const;
|
|
129
|
-
export const DEFAULT_BUTTON_LAYOUT = BUTTON_LAYOUT_VALUES[0];
|
|
130
|
-
|
|
131
5
|
export const EFFECT_VALUES = ["confetti"] as const;
|
|
132
|
-
|
|
133
|
-
export const GROUP_LAYOUT_VALUES = ["row"] as const;
|
|
134
|
-
|
|
135
|
-
/** Only valid as `page.elements`: vertical container for the page body (matches json-render trees). */
|
|
136
|
-
export const PAGE_ROOT_TYPE = {
|
|
137
|
-
stack: "stack",
|
|
138
|
-
} as const;
|
|
139
|
-
|
|
140
|
-
export const ELEMENT_TYPE = {
|
|
141
|
-
text: "text",
|
|
142
|
-
image: "image",
|
|
143
|
-
divider: "divider",
|
|
144
|
-
spacer: "spacer",
|
|
145
|
-
progress: "progress",
|
|
146
|
-
list: "list",
|
|
147
|
-
grid: "grid",
|
|
148
|
-
text_input: "text_input",
|
|
149
|
-
slider: "slider",
|
|
150
|
-
button_group: "button_group",
|
|
151
|
-
toggle: "toggle",
|
|
152
|
-
group: "group",
|
|
153
|
-
bar_chart: "bar_chart",
|
|
154
|
-
} as const;
|
|
155
|
-
|
|
156
|
-
export type ElementType = (typeof ELEMENT_TYPE)[keyof typeof ELEMENT_TYPE];
|
|
157
|
-
|
|
158
|
-
export const HTTPS_PREFIX = "https://" as const;
|
|
159
|
-
export const HTTP_PREFIX = "http://" as const;
|
|
160
|
-
|
|
161
|
-
/** 6-digit hex only (#RRGGBB); used for grid cell backgrounds (free hex). */
|
|
162
|
-
export const HEX_COLOR_6_RE = /^#[0-9a-fA-F]{6}$/;
|
|
163
|
-
|
|
164
|
-
export const TEXT_CONTENT_MAX = {
|
|
165
|
-
[TEXT_STYLE.title]: 80,
|
|
166
|
-
[TEXT_STYLE.body]: 160,
|
|
167
|
-
[TEXT_STYLE.caption]: 100,
|
|
168
|
-
[TEXT_STYLE.label]: 40,
|
|
169
|
-
} as const satisfies Record<(typeof TEXT_STYLE_VALUES)[number], number>;
|
|
170
|
-
|
|
171
|
-
export const SLIDER_STEP_ALIGN_EPS = 1e-6;
|
|
172
|
-
export const DEFAULT_SLIDER_STEP = 1 as const;
|
|
173
|
-
|
|
174
|
-
export const MEDIA_ELEMENT_TYPES = [
|
|
175
|
-
ELEMENT_TYPE.image,
|
|
176
|
-
ELEMENT_TYPE.grid,
|
|
177
|
-
] as ElementType[];
|
|
178
|
-
|
|
179
|
-
export const INTERACTIVE_ELEMENT_TYPES = [
|
|
180
|
-
ELEMENT_TYPE.button_group,
|
|
181
|
-
ELEMENT_TYPE.slider,
|
|
182
|
-
ELEMENT_TYPE.text_input,
|
|
183
|
-
ELEMENT_TYPE.toggle,
|
|
184
|
-
] as ElementType[];
|
package/src/dataStore.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type DataStoreValue =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| boolean
|
|
5
|
+
| null
|
|
6
|
+
| DataStoreValue[]
|
|
7
|
+
| { [key: string]: DataStoreValue };
|
|
8
|
+
|
|
9
|
+
export type SnapDataStoreOperations = {
|
|
10
|
+
get(key: string): Promise<DataStoreValue | null>;
|
|
11
|
+
set(key: string, value: DataStoreValue): Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type SnapDataStore = SnapDataStoreOperations & {
|
|
15
|
+
withLock<T>(fn: (store: SnapDataStoreOperations) => Promise<T>): Promise<T>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createDefaultDataStore(): SnapDataStore {
|
|
19
|
+
const err = new Error(
|
|
20
|
+
"Data store is not configured. Use withUpstash() from @farcaster/snap-upstash or provide a data store implementation.",
|
|
21
|
+
);
|
|
22
|
+
return {
|
|
23
|
+
get(_key: string): Promise<never> {
|
|
24
|
+
return Promise.reject(err);
|
|
25
|
+
},
|
|
26
|
+
set(_key: string, _value: DataStoreValue): Promise<never> {
|
|
27
|
+
return Promise.reject(err);
|
|
28
|
+
},
|
|
29
|
+
withLock<T>(
|
|
30
|
+
_fn: (store: SnapDataStoreOperations) => Promise<T>,
|
|
31
|
+
): Promise<never> {
|
|
32
|
+
return Promise.reject(err);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createInMemoryDataStore(): SnapDataStore {
|
|
38
|
+
const data = new Map<string, DataStoreValue>();
|
|
39
|
+
const ops: SnapDataStoreOperations = {
|
|
40
|
+
get: async (key: string): Promise<DataStoreValue | null> => {
|
|
41
|
+
return data.get(key) ?? null;
|
|
42
|
+
},
|
|
43
|
+
set: async (key: string, value: DataStoreValue): Promise<void> => {
|
|
44
|
+
data.set(key, value);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** Serializes `withLock` callbacks so async work does not interleave across callers. */
|
|
48
|
+
let lockChain: Promise<unknown> = Promise.resolve();
|
|
49
|
+
return {
|
|
50
|
+
...ops,
|
|
51
|
+
withLock<T>(
|
|
52
|
+
fn: (store: SnapDataStoreOperations) => Promise<T>,
|
|
53
|
+
): Promise<T> {
|
|
54
|
+
const run = lockChain.then(() => fn(ops));
|
|
55
|
+
lockChain = run.then(
|
|
56
|
+
() => undefined,
|
|
57
|
+
() => undefined,
|
|
58
|
+
);
|
|
59
|
+
return run;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
+
export type { Spec as SnapSpec, UIElement as SnapUIElement } from "@json-render/core";
|
|
1
2
|
export {
|
|
2
|
-
|
|
3
|
-
PAGE_ROOT_TYPE,
|
|
4
|
-
ELEMENT_TYPE,
|
|
3
|
+
SPEC_VERSION,
|
|
5
4
|
MEDIA_TYPE,
|
|
6
|
-
|
|
7
|
-
DEFAULT_SLIDER_STEP,
|
|
8
|
-
CLIENT_ACTION,
|
|
9
|
-
CLIENT_ACTION_VALUES,
|
|
5
|
+
EFFECT_VALUES,
|
|
10
6
|
} from "./constants";
|
|
11
7
|
export {
|
|
12
8
|
DEFAULT_THEME_ACCENT,
|
|
@@ -21,28 +17,23 @@ export {
|
|
|
21
17
|
ACTION_TYPE_GET,
|
|
22
18
|
ACTION_TYPE_POST,
|
|
23
19
|
snapResponseSchema,
|
|
24
|
-
firstPageResponseSchema,
|
|
25
20
|
payloadSchema,
|
|
26
|
-
clientActionSchema,
|
|
27
|
-
createDefaultDataStore,
|
|
28
|
-
type Button,
|
|
29
|
-
type Element,
|
|
30
|
-
type Elements,
|
|
31
|
-
type GroupChildElement,
|
|
32
|
-
type ClientAction,
|
|
33
21
|
type SnapAction,
|
|
34
|
-
type SnapPageElementInput,
|
|
35
22
|
type SnapContext,
|
|
36
23
|
type SnapResponse,
|
|
37
24
|
type SnapHandlerResult,
|
|
38
25
|
type SnapFunction,
|
|
39
26
|
type SnapPayload,
|
|
40
|
-
type DataStoreValue,
|
|
41
|
-
type SnapDataStore,
|
|
42
|
-
type SnapDataStoreOperations,
|
|
43
27
|
} from "./schemas";
|
|
44
28
|
export {
|
|
45
29
|
validateSnapResponse,
|
|
46
|
-
validateFirstPageResponse,
|
|
47
30
|
type ValidationResult,
|
|
48
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";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
type SnapPreviewAccentContextValue = {
|
|
6
|
+
/** From loaded snap `page.theme.accent` (undefined if the snap omits it). */
|
|
7
|
+
pageAccent: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SnapPreviewAccentContext =
|
|
11
|
+
createContext<SnapPreviewAccentContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
export function SnapPreviewAccentProvider({
|
|
14
|
+
pageAccent,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
pageAccent: string | undefined;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<SnapPreviewAccentContext.Provider value={{ pageAccent }}>
|
|
22
|
+
{children}
|
|
23
|
+
</SnapPreviewAccentContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useSnapPreviewPageAccent(): string | undefined {
|
|
28
|
+
return useContext(SnapPreviewAccentContext)?.pageAccent;
|
|
29
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createRenderer } from "@json-render/react";
|
|
4
|
+
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
5
|
+
import { SnapActionButton } from "./components/action-button";
|
|
6
|
+
import { SnapBadge } from "./components/badge";
|
|
7
|
+
import { SnapIcon } from "./components/icon";
|
|
8
|
+
import { SnapImage } from "./components/image";
|
|
9
|
+
import { SnapInput } from "./components/input";
|
|
10
|
+
import { SnapItem } from "./components/item";
|
|
11
|
+
import { SnapItemGroup } from "./components/item-group";
|
|
12
|
+
import { SnapProgress } from "./components/progress";
|
|
13
|
+
import { SnapSeparator } from "./components/separator";
|
|
14
|
+
import { SnapSlider } from "./components/slider";
|
|
15
|
+
import { SnapStack } from "./components/stack";
|
|
16
|
+
import { SnapSwitch } from "./components/switch";
|
|
17
|
+
import { SnapText } from "./components/text";
|
|
18
|
+
import { SnapToggleGroup } from "./components/toggle-group";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maps snap json-render catalog types to React components.
|
|
22
|
+
* Keys match the snap wire-format `type` strings exactly.
|
|
23
|
+
*/
|
|
24
|
+
export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
|
|
25
|
+
badge: SnapBadge,
|
|
26
|
+
button: SnapActionButton,
|
|
27
|
+
icon: SnapIcon,
|
|
28
|
+
image: SnapImage,
|
|
29
|
+
input: SnapInput,
|
|
30
|
+
item: SnapItem,
|
|
31
|
+
item_group: SnapItemGroup,
|
|
32
|
+
progress: SnapProgress,
|
|
33
|
+
separator: SnapSeparator,
|
|
34
|
+
slider: SnapSlider,
|
|
35
|
+
stack: SnapStack,
|
|
36
|
+
switch: SnapSwitch,
|
|
37
|
+
text: SnapText,
|
|
38
|
+
toggle_group: SnapToggleGroup,
|
|
39
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@neynar/ui/button";
|
|
4
|
+
import { cn } from "@neynar/ui/utils";
|
|
5
|
+
import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
|
|
6
|
+
import { ICON_MAP } from "./icon";
|
|
7
|
+
|
|
8
|
+
const VARIANT_MAP: Record<string, "default" | "outline" | "ghost" | "secondary"> = {
|
|
9
|
+
default: "default",
|
|
10
|
+
secondary: "secondary",
|
|
11
|
+
outline: "outline",
|
|
12
|
+
ghost: "ghost",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function SnapActionButton({
|
|
16
|
+
element: { props },
|
|
17
|
+
emit,
|
|
18
|
+
}: {
|
|
19
|
+
element: { props: Record<string, unknown> };
|
|
20
|
+
emit: (name: string) => void;
|
|
21
|
+
}) {
|
|
22
|
+
const label = String(props.label ?? "Action");
|
|
23
|
+
const variant = VARIANT_MAP[String(props.variant ?? "default")] ?? "default";
|
|
24
|
+
const iconName = props.icon ? String(props.icon) : undefined;
|
|
25
|
+
const accentStyle = useSnapAccentScopeStyle();
|
|
26
|
+
|
|
27
|
+
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="w-full min-w-0 flex-1" style={accentStyle}>
|
|
31
|
+
<Button
|
|
32
|
+
type="button"
|
|
33
|
+
variant={variant}
|
|
34
|
+
className={cn(
|
|
35
|
+
"w-full gap-2",
|
|
36
|
+
variant === "default" &&
|
|
37
|
+
"hover:!bg-[var(--snap-action-primary-hover)]",
|
|
38
|
+
variant !== "default" &&
|
|
39
|
+
"hover:!bg-[var(--snap-action-outline-hover)]",
|
|
40
|
+
)}
|
|
41
|
+
onClick={() => emit("press")}
|
|
42
|
+
>
|
|
43
|
+
{Icon && <Icon size={16} />}
|
|
44
|
+
{label}
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@neynar/ui/badge";
|
|
4
|
+
import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
|
|
5
|
+
import { ICON_MAP } from "./icon";
|
|
6
|
+
|
|
7
|
+
export function SnapBadge({
|
|
8
|
+
element: { props },
|
|
9
|
+
}: {
|
|
10
|
+
element: { props: Record<string, unknown> };
|
|
11
|
+
}) {
|
|
12
|
+
const content = String(props.label ?? "");
|
|
13
|
+
const color = props.color ? String(props.color) : undefined;
|
|
14
|
+
const iconName = props.icon ? String(props.icon) : undefined;
|
|
15
|
+
const accentStyle = useSnapAccentScopeStyle();
|
|
16
|
+
|
|
17
|
+
const isAccent = !color || color === "accent";
|
|
18
|
+
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<span style={isAccent ? accentStyle : undefined}>
|
|
22
|
+
<Badge
|
|
23
|
+
variant={isAccent ? "default" : "outline"}
|
|
24
|
+
className="gap-1"
|
|
25
|
+
// TODO: fix outline badge border color in @neynar/ui — too bright in dark mode
|
|
26
|
+
style={
|
|
27
|
+
!isAccent
|
|
28
|
+
? { borderColor: `var(--snap-color-${color})`, color: `var(--snap-color-${color})` }
|
|
29
|
+
: undefined
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
{Icon && <Icon size={12} />}
|
|
33
|
+
{content}
|
|
34
|
+
</Badge>
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ArrowRight,
|
|
5
|
+
ArrowLeft,
|
|
6
|
+
ExternalLink,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
Check,
|
|
9
|
+
X,
|
|
10
|
+
AlertTriangle,
|
|
11
|
+
Info,
|
|
12
|
+
Clock,
|
|
13
|
+
Heart,
|
|
14
|
+
MessageCircle,
|
|
15
|
+
Repeat,
|
|
16
|
+
Share,
|
|
17
|
+
User,
|
|
18
|
+
Users,
|
|
19
|
+
Star,
|
|
20
|
+
Trophy,
|
|
21
|
+
Zap,
|
|
22
|
+
Flame,
|
|
23
|
+
Gift,
|
|
24
|
+
ImageIcon,
|
|
25
|
+
Play,
|
|
26
|
+
Pause,
|
|
27
|
+
Wallet,
|
|
28
|
+
Coins,
|
|
29
|
+
Plus,
|
|
30
|
+
Minus,
|
|
31
|
+
RefreshCw,
|
|
32
|
+
Bookmark,
|
|
33
|
+
ThumbsUp,
|
|
34
|
+
ThumbsDown,
|
|
35
|
+
TrendingUp,
|
|
36
|
+
TrendingDown,
|
|
37
|
+
type LucideIcon,
|
|
38
|
+
} from "lucide-react";
|
|
39
|
+
import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
|
|
40
|
+
|
|
41
|
+
export const ICON_MAP: Record<string, LucideIcon> = {
|
|
42
|
+
"arrow-right": ArrowRight,
|
|
43
|
+
"arrow-left": ArrowLeft,
|
|
44
|
+
"external-link": ExternalLink,
|
|
45
|
+
"chevron-right": ChevronRight,
|
|
46
|
+
check: Check,
|
|
47
|
+
x: X,
|
|
48
|
+
"alert-triangle": AlertTriangle,
|
|
49
|
+
info: Info,
|
|
50
|
+
clock: Clock,
|
|
51
|
+
heart: Heart,
|
|
52
|
+
"message-circle": MessageCircle,
|
|
53
|
+
repeat: Repeat,
|
|
54
|
+
share: Share,
|
|
55
|
+
user: User,
|
|
56
|
+
users: Users,
|
|
57
|
+
star: Star,
|
|
58
|
+
trophy: Trophy,
|
|
59
|
+
zap: Zap,
|
|
60
|
+
flame: Flame,
|
|
61
|
+
gift: Gift,
|
|
62
|
+
image: ImageIcon,
|
|
63
|
+
play: Play,
|
|
64
|
+
pause: Pause,
|
|
65
|
+
wallet: Wallet,
|
|
66
|
+
coins: Coins,
|
|
67
|
+
plus: Plus,
|
|
68
|
+
minus: Minus,
|
|
69
|
+
"refresh-cw": RefreshCw,
|
|
70
|
+
bookmark: Bookmark,
|
|
71
|
+
"thumbs-up": ThumbsUp,
|
|
72
|
+
"thumbs-down": ThumbsDown,
|
|
73
|
+
"trending-up": TrendingUp,
|
|
74
|
+
"trending-down": TrendingDown,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const SIZE_PX: Record<string, number> = {
|
|
78
|
+
sm: 16,
|
|
79
|
+
md: 20,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function SnapIcon({
|
|
83
|
+
element: { props },
|
|
84
|
+
}: {
|
|
85
|
+
element: { props: Record<string, unknown> };
|
|
86
|
+
}) {
|
|
87
|
+
const name = String(props.name ?? "info");
|
|
88
|
+
const size = SIZE_PX[String(props.size ?? "md")] ?? 20;
|
|
89
|
+
const color = props.color ? String(props.color) : undefined;
|
|
90
|
+
const accentStyle = useSnapAccentScopeStyle();
|
|
91
|
+
|
|
92
|
+
const Icon = ICON_MAP[name];
|
|
93
|
+
if (!Icon) return null;
|
|
94
|
+
|
|
95
|
+
const isAccent = !color || color === "accent";
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<span
|
|
99
|
+
style={{
|
|
100
|
+
display: "inline-flex",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
...(isAccent ? accentStyle : {}),
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<Icon
|
|
106
|
+
size={size}
|
|
107
|
+
style={
|
|
108
|
+
isAccent
|
|
109
|
+
? { color: "var(--snap-accent, currentColor)" }
|
|
110
|
+
: { color: `var(--snap-color-${color}, currentColor)` }
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
</span>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AspectRatio } from "@neynar/ui/aspect-ratio";
|
|
4
|
+
|
|
5
|
+
function aspectToRatio(aspect: string): number {
|
|
6
|
+
const [w, h] = aspect.split(":").map(Number);
|
|
7
|
+
if (!w || !h) return 1;
|
|
8
|
+
return w / h;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SnapImage({
|
|
12
|
+
element: { props },
|
|
13
|
+
}: {
|
|
14
|
+
element: { props: Record<string, unknown> };
|
|
15
|
+
}) {
|
|
16
|
+
const url = String(props.url ?? "");
|
|
17
|
+
const alt = String(props.alt ?? "");
|
|
18
|
+
const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<AspectRatio
|
|
22
|
+
ratio={ratio}
|
|
23
|
+
className="relative w-full flex-1 overflow-hidden rounded-lg"
|
|
24
|
+
>
|
|
25
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
26
|
+
<img
|
|
27
|
+
src={url}
|
|
28
|
+
alt={alt}
|
|
29
|
+
className="absolute inset-0 size-full object-cover"
|
|
30
|
+
/>
|
|
31
|
+
</AspectRatio>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { useStateStore } from "@json-render/react";
|
|
5
|
+
import { Input } from "@neynar/ui/input";
|
|
6
|
+
import { Label } from "@neynar/ui/label";
|
|
7
|
+
|
|
8
|
+
export function SnapInput({
|
|
9
|
+
element: { props },
|
|
10
|
+
}: {
|
|
11
|
+
element: { props: Record<string, unknown> };
|
|
12
|
+
}) {
|
|
13
|
+
const id = useId();
|
|
14
|
+
const { get, set } = useStateStore();
|
|
15
|
+
const name = String(props.name ?? "input");
|
|
16
|
+
const path = `/inputs/${name}`;
|
|
17
|
+
const label = props.label ? String(props.label) : undefined;
|
|
18
|
+
const placeholder = props.placeholder ? String(props.placeholder) : undefined;
|
|
19
|
+
const maxLength =
|
|
20
|
+
typeof props.maxLength === "number" ? props.maxLength : undefined;
|
|
21
|
+
const raw = get(path);
|
|
22
|
+
const value = typeof raw === "string" ? raw : "";
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="w-full space-y-1.5">
|
|
26
|
+
{label && <Label htmlFor={id}>{label}</Label>}
|
|
27
|
+
<Input
|
|
28
|
+
id={id}
|
|
29
|
+
value={value}
|
|
30
|
+
onChange={(e) => set(path, e.target.value)}
|
|
31
|
+
placeholder={placeholder}
|
|
32
|
+
maxLength={maxLength}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Children, type ReactNode, Fragment } from "react";
|
|
4
|
+
import { cn } from "@neynar/ui/utils";
|
|
5
|
+
|
|
6
|
+
const GAP_MAP: Record<string, string> = {
|
|
7
|
+
none: "gap-0",
|
|
8
|
+
sm: "gap-1",
|
|
9
|
+
md: "gap-2",
|
|
10
|
+
lg: "gap-3",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function SnapItemGroup({
|
|
14
|
+
element: { props },
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
element: { props: Record<string, unknown> };
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
const border = Boolean(props.border);
|
|
21
|
+
const separator = Boolean(props.separator);
|
|
22
|
+
const gap = GAP_MAP[String(props.gap ?? "sm")] ?? "gap-1";
|
|
23
|
+
const items = Children.toArray(children);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
"flex flex-col",
|
|
29
|
+
border && "rounded-lg border",
|
|
30
|
+
gap,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
{items.map((child, i) => (
|
|
34
|
+
<Fragment key={i}>
|
|
35
|
+
{separator && i > 0 && (
|
|
36
|
+
<div className="h-px bg-border" />
|
|
37
|
+
)}
|
|
38
|
+
{child}
|
|
39
|
+
</Fragment>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|