@farcaster/snap 2.3.1 → 2.5.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/button-orientation-utils.d.ts +3 -0
- package/dist/button-orientation-utils.js +25 -0
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +2 -1
- package/dist/react/components/action-button.js +5 -5
- package/dist/react/components/cell-grid.js +6 -2
- package/dist/react/components/item-group.js +2 -1
- package/dist/react/components/item-layout-context.d.ts +2 -0
- package/dist/react/components/item-layout-context.js +7 -0
- package/dist/react/components/item.js +40 -3
- package/dist/react/components/stack.js +46 -37
- package/dist/react/components/toggle-group.js +6 -4
- package/dist/react/snap-view-core.js +107 -17
- package/dist/react-native/components/item-layout-context.d.ts +2 -0
- package/dist/react-native/components/item-layout-context.js +6 -0
- package/dist/react-native/components/snap-action-button.js +15 -2
- package/dist/react-native/components/snap-cell-grid.js +30 -4
- package/dist/react-native/components/snap-item-group.js +16 -6
- package/dist/react-native/components/snap-item.js +56 -13
- package/dist/react-native/components/snap-stack.js +32 -33
- package/dist/react-native/components/snap-toggle-group.js +5 -4
- package/dist/react-native/fireworks-overlay.d.ts +1 -0
- package/dist/react-native/fireworks-overlay.js +125 -0
- package/dist/react-native/snap-view-core.js +7 -2
- package/dist/schemas.d.ts +1 -0
- package/dist/stack-horizontal-utils.d.ts +7 -5
- package/dist/stack-horizontal-utils.js +24 -14
- package/dist/ui/catalog.d.ts +60 -1
- package/dist/ui/catalog.js +3 -3
- package/dist/ui/cell-grid.d.ts +4 -0
- package/dist/ui/cell-grid.js +3 -4
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +1 -1
- package/dist/ui/item.d.ts +112 -1
- package/dist/ui/item.js +28 -2
- package/dist/ui/stack.d.ts +1 -0
- package/dist/ui/stack.js +3 -1
- package/dist/validator.js +19 -1
- package/llms.txt +3 -1
- package/package.json +1 -1
- package/src/button-orientation-utils.ts +36 -0
- package/src/constants.ts +2 -1
- package/src/react/components/action-button.tsx +5 -4
- package/src/react/components/cell-grid.tsx +6 -2
- package/src/react/components/item-group.tsx +19 -17
- package/src/react/components/item-layout-context.tsx +12 -0
- package/src/react/components/item.tsx +97 -4
- package/src/react/components/stack.tsx +51 -40
- package/src/react/components/toggle-group.tsx +6 -4
- package/src/react/snap-view-core.tsx +152 -28
- package/src/react-native/components/item-layout-context.tsx +10 -0
- package/src/react-native/components/snap-action-button.tsx +15 -2
- package/src/react-native/components/snap-cell-grid.tsx +36 -4
- package/src/react-native/components/snap-item-group.tsx +31 -17
- package/src/react-native/components/snap-item.tsx +92 -14
- package/src/react-native/components/snap-stack.tsx +37 -36
- package/src/react-native/components/snap-toggle-group.tsx +5 -4
- package/src/react-native/fireworks-overlay.tsx +176 -0
- package/src/react-native/snap-view-core.tsx +6 -1
- package/src/stack-horizontal-utils.ts +32 -13
- package/src/ui/catalog.ts +5 -4
- package/src/ui/cell-grid.ts +5 -5
- package/src/ui/index.ts +2 -2
- package/src/ui/item.ts +35 -5
- package/src/ui/stack.ts +3 -1
- package/src/validator.ts +29 -1
package/dist/ui/catalog.d.ts
CHANGED
|
@@ -186,7 +186,61 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
186
186
|
variant: z.ZodOptional<z.ZodEnum<{
|
|
187
187
|
default: "default";
|
|
188
188
|
}>>;
|
|
189
|
-
|
|
189
|
+
media: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
190
|
+
variant: z.ZodLiteral<"icon">;
|
|
191
|
+
name: z.ZodEnum<{
|
|
192
|
+
"arrow-right": "arrow-right";
|
|
193
|
+
"arrow-left": "arrow-left";
|
|
194
|
+
"external-link": "external-link";
|
|
195
|
+
"chevron-right": "chevron-right";
|
|
196
|
+
check: "check";
|
|
197
|
+
x: "x";
|
|
198
|
+
"alert-triangle": "alert-triangle";
|
|
199
|
+
info: "info";
|
|
200
|
+
clock: "clock";
|
|
201
|
+
heart: "heart";
|
|
202
|
+
"message-circle": "message-circle";
|
|
203
|
+
repeat: "repeat";
|
|
204
|
+
share: "share";
|
|
205
|
+
user: "user";
|
|
206
|
+
users: "users";
|
|
207
|
+
star: "star";
|
|
208
|
+
trophy: "trophy";
|
|
209
|
+
zap: "zap";
|
|
210
|
+
flame: "flame";
|
|
211
|
+
gift: "gift";
|
|
212
|
+
image: "image";
|
|
213
|
+
play: "play";
|
|
214
|
+
pause: "pause";
|
|
215
|
+
wallet: "wallet";
|
|
216
|
+
coins: "coins";
|
|
217
|
+
plus: "plus";
|
|
218
|
+
minus: "minus";
|
|
219
|
+
"refresh-cw": "refresh-cw";
|
|
220
|
+
bookmark: "bookmark";
|
|
221
|
+
"thumbs-up": "thumbs-up";
|
|
222
|
+
"thumbs-down": "thumbs-down";
|
|
223
|
+
"trending-up": "trending-up";
|
|
224
|
+
"trending-down": "trending-down";
|
|
225
|
+
}>;
|
|
226
|
+
color: z.ZodOptional<z.ZodEnum<{
|
|
227
|
+
gray: "gray";
|
|
228
|
+
blue: "blue";
|
|
229
|
+
red: "red";
|
|
230
|
+
amber: "amber";
|
|
231
|
+
green: "green";
|
|
232
|
+
teal: "teal";
|
|
233
|
+
purple: "purple";
|
|
234
|
+
pink: "pink";
|
|
235
|
+
accent: "accent";
|
|
236
|
+
}>>;
|
|
237
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
238
|
+
variant: z.ZodLiteral<"image">;
|
|
239
|
+
url: z.ZodString;
|
|
240
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
241
|
+
round: z.ZodOptional<z.ZodBoolean>;
|
|
242
|
+
}, z.core.$strict>], "variant">>;
|
|
243
|
+
}, z.core.$strict>;
|
|
190
244
|
description: string;
|
|
191
245
|
};
|
|
192
246
|
item_group: {
|
|
@@ -318,6 +372,7 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
318
372
|
between: "between";
|
|
319
373
|
around: "around";
|
|
320
374
|
}>>;
|
|
375
|
+
equalWidth: z.ZodOptional<z.ZodBoolean>;
|
|
321
376
|
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
|
|
322
377
|
}, z.core.$strip>;
|
|
323
378
|
description: string;
|
|
@@ -399,6 +454,10 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
399
454
|
none: "none";
|
|
400
455
|
lg: "lg";
|
|
401
456
|
}>>;
|
|
457
|
+
cellAspectRatio: z.ZodOptional<z.ZodEnum<{
|
|
458
|
+
auto: "auto";
|
|
459
|
+
square: "square";
|
|
460
|
+
}>>;
|
|
402
461
|
rowHeight: z.ZodOptional<z.ZodNumber>;
|
|
403
462
|
select: z.ZodOptional<z.ZodEnum<{
|
|
404
463
|
multiple: "multiple";
|
package/dist/ui/catalog.js
CHANGED
|
@@ -42,7 +42,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
42
42
|
},
|
|
43
43
|
toggle_group: {
|
|
44
44
|
props: toggleGroupProps,
|
|
45
|
-
description: "Single or multi-select choice group; `name` becomes POST inputs key.
|
|
45
|
+
description: "Single or multi-select choice group; `name` becomes POST inputs key. The @farcaster/snap React/React Native components choose row/column orientation from option label length, ignoring snap-sent orientation hints.",
|
|
46
46
|
},
|
|
47
47
|
input: {
|
|
48
48
|
props: inputProps,
|
|
@@ -50,7 +50,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
50
50
|
},
|
|
51
51
|
item: {
|
|
52
52
|
props: itemProps,
|
|
53
|
-
description: "Content row
|
|
53
|
+
description: "Content row matching shadcn Item: optional media renders on the left, title and optional description render in the content area, and children render in the actions slot (right side). The item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.",
|
|
54
54
|
},
|
|
55
55
|
item_group: {
|
|
56
56
|
props: itemGroupProps,
|
|
@@ -78,7 +78,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
78
78
|
},
|
|
79
79
|
stack: {
|
|
80
80
|
props: stackProps,
|
|
81
|
-
description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order. Horizontal stacks use a single flex row so peers stay side-by-side and shrink with min-width 0. Nested stacks participate as flexible row peers.
|
|
81
|
+
description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order. Horizontal stacks use a single flex row so peers stay side-by-side and shrink with min-width 0. Nested stacks participate as flexible row peers. For all-button stacks, the @farcaster/snap React/React Native components choose row/column orientation from button label length, ignoring snap-sent direction hints; horizontal button rows use content-proportional widths while filling the container unless `equalWidth: true` is provided to force equal-width cells. Vertical button rows default to a tighter gap when `gap` is omitted.",
|
|
82
82
|
},
|
|
83
83
|
text: {
|
|
84
84
|
props: textProps,
|
package/dist/ui/cell-grid.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export declare const cellGridProps: z.ZodObject<{
|
|
|
25
25
|
none: "none";
|
|
26
26
|
lg: "lg";
|
|
27
27
|
}>>;
|
|
28
|
+
cellAspectRatio: z.ZodOptional<z.ZodEnum<{
|
|
29
|
+
auto: "auto";
|
|
30
|
+
square: "square";
|
|
31
|
+
}>>;
|
|
28
32
|
rowHeight: z.ZodOptional<z.ZodNumber>;
|
|
29
33
|
select: z.ZodOptional<z.ZodEnum<{
|
|
30
34
|
multiple: "multiple";
|
package/dist/ui/cell-grid.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { isSnapHexColorString, PALETTE_COLOR_VALUES } from "../colors.js";
|
|
3
|
-
import { GRID_MIN_COLS, GRID_MAX_COLS, GRID_MIN_ROWS, GRID_MAX_ROWS, GRID_GAP_VALUES, } from "../constants.js";
|
|
3
|
+
import { GRID_MIN_COLS, GRID_MAX_COLS, GRID_MIN_ROWS, GRID_MAX_ROWS, GRID_GAP_VALUES, GRID_CELL_ASPECT_RATIO_VALUES, } from "../constants.js";
|
|
4
4
|
/** Palette name or `#rrggbb`; input is trimmed so palette and hex rules match runtime resolvers. */
|
|
5
5
|
const cellGridCellColorSchema = z.preprocess((v) => (typeof v === "string" ? v.trim() : v), z.union([
|
|
6
6
|
z.enum(PALETTE_COLOR_VALUES),
|
|
7
|
-
z
|
|
8
|
-
.string()
|
|
9
|
-
.refine(isSnapHexColorString, {
|
|
7
|
+
z.string().refine(isSnapHexColorString, {
|
|
10
8
|
message: "cell_grid cell hex color must be #rrggbb",
|
|
11
9
|
}),
|
|
12
10
|
]));
|
|
@@ -24,6 +22,7 @@ export const cellGridProps = z
|
|
|
24
22
|
rows: z.number().int().min(GRID_MIN_ROWS).max(GRID_MAX_ROWS),
|
|
25
23
|
cells: z.array(cellGridCellSchema),
|
|
26
24
|
gap: z.enum(GRID_GAP_VALUES).optional(),
|
|
25
|
+
cellAspectRatio: z.enum(GRID_CELL_ASPECT_RATIO_VALUES).optional(),
|
|
27
26
|
rowHeight: z.number().int().min(8).max(64).optional(),
|
|
28
27
|
select: z.enum(["off", "single", "multiple"]).optional(),
|
|
29
28
|
})
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export { toggleGroupProps } from "./toggle-group.js";
|
|
|
10
10
|
export type { ToggleGroupProps } from "./toggle-group.js";
|
|
11
11
|
export { inputProps } from "./input.js";
|
|
12
12
|
export type { InputProps } from "./input.js";
|
|
13
|
-
export { itemProps } from "./item.js";
|
|
14
|
-
export type { ItemProps } from "./item.js";
|
|
13
|
+
export { itemProps, itemMediaProps } from "./item.js";
|
|
14
|
+
export type { ItemMediaProps, ItemProps } from "./item.js";
|
|
15
15
|
export { itemGroupProps } from "./item-group.js";
|
|
16
16
|
export type { ItemGroupProps } from "./item-group.js";
|
|
17
17
|
export { iconProps, ICON_NAMES } from "./icon.js";
|
package/dist/ui/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { buttonProps } from "./button.js";
|
|
|
5
5
|
export { switchProps } from "./switch.js";
|
|
6
6
|
export { toggleGroupProps } from "./toggle-group.js";
|
|
7
7
|
export { inputProps } from "./input.js";
|
|
8
|
-
export { itemProps } from "./item.js";
|
|
8
|
+
export { itemProps, itemMediaProps } from "./item.js";
|
|
9
9
|
export { itemGroupProps } from "./item-group.js";
|
|
10
10
|
export { iconProps, ICON_NAMES } from "./icon.js";
|
|
11
11
|
export { imageProps } from "./image.js";
|
package/dist/ui/item.d.ts
CHANGED
|
@@ -1,12 +1,123 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const ITEM_VARIANTS: readonly ["default"];
|
|
3
|
+
export declare const ITEM_MEDIA_VARIANTS: readonly ["icon", "image"];
|
|
3
4
|
export declare const ITEM_MAX_TITLE_CHARS = 100;
|
|
4
5
|
export declare const ITEM_MAX_DESCRIPTION_CHARS = 160;
|
|
6
|
+
export declare const ITEM_MAX_MEDIA_ALT_CHARS = 120;
|
|
7
|
+
export declare const itemMediaProps: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
8
|
+
variant: z.ZodLiteral<"icon">;
|
|
9
|
+
name: z.ZodEnum<{
|
|
10
|
+
"arrow-right": "arrow-right";
|
|
11
|
+
"arrow-left": "arrow-left";
|
|
12
|
+
"external-link": "external-link";
|
|
13
|
+
"chevron-right": "chevron-right";
|
|
14
|
+
check: "check";
|
|
15
|
+
x: "x";
|
|
16
|
+
"alert-triangle": "alert-triangle";
|
|
17
|
+
info: "info";
|
|
18
|
+
clock: "clock";
|
|
19
|
+
heart: "heart";
|
|
20
|
+
"message-circle": "message-circle";
|
|
21
|
+
repeat: "repeat";
|
|
22
|
+
share: "share";
|
|
23
|
+
user: "user";
|
|
24
|
+
users: "users";
|
|
25
|
+
star: "star";
|
|
26
|
+
trophy: "trophy";
|
|
27
|
+
zap: "zap";
|
|
28
|
+
flame: "flame";
|
|
29
|
+
gift: "gift";
|
|
30
|
+
image: "image";
|
|
31
|
+
play: "play";
|
|
32
|
+
pause: "pause";
|
|
33
|
+
wallet: "wallet";
|
|
34
|
+
coins: "coins";
|
|
35
|
+
plus: "plus";
|
|
36
|
+
minus: "minus";
|
|
37
|
+
"refresh-cw": "refresh-cw";
|
|
38
|
+
bookmark: "bookmark";
|
|
39
|
+
"thumbs-up": "thumbs-up";
|
|
40
|
+
"thumbs-down": "thumbs-down";
|
|
41
|
+
"trending-up": "trending-up";
|
|
42
|
+
"trending-down": "trending-down";
|
|
43
|
+
}>;
|
|
44
|
+
color: z.ZodOptional<z.ZodEnum<{
|
|
45
|
+
gray: "gray";
|
|
46
|
+
blue: "blue";
|
|
47
|
+
red: "red";
|
|
48
|
+
amber: "amber";
|
|
49
|
+
green: "green";
|
|
50
|
+
teal: "teal";
|
|
51
|
+
purple: "purple";
|
|
52
|
+
pink: "pink";
|
|
53
|
+
accent: "accent";
|
|
54
|
+
}>>;
|
|
55
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
56
|
+
variant: z.ZodLiteral<"image">;
|
|
57
|
+
url: z.ZodString;
|
|
58
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
59
|
+
round: z.ZodOptional<z.ZodBoolean>;
|
|
60
|
+
}, z.core.$strict>], "variant">;
|
|
5
61
|
export declare const itemProps: z.ZodObject<{
|
|
6
62
|
title: z.ZodString;
|
|
7
63
|
description: z.ZodOptional<z.ZodString>;
|
|
8
64
|
variant: z.ZodOptional<z.ZodEnum<{
|
|
9
65
|
default: "default";
|
|
10
66
|
}>>;
|
|
11
|
-
|
|
67
|
+
media: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
68
|
+
variant: z.ZodLiteral<"icon">;
|
|
69
|
+
name: z.ZodEnum<{
|
|
70
|
+
"arrow-right": "arrow-right";
|
|
71
|
+
"arrow-left": "arrow-left";
|
|
72
|
+
"external-link": "external-link";
|
|
73
|
+
"chevron-right": "chevron-right";
|
|
74
|
+
check: "check";
|
|
75
|
+
x: "x";
|
|
76
|
+
"alert-triangle": "alert-triangle";
|
|
77
|
+
info: "info";
|
|
78
|
+
clock: "clock";
|
|
79
|
+
heart: "heart";
|
|
80
|
+
"message-circle": "message-circle";
|
|
81
|
+
repeat: "repeat";
|
|
82
|
+
share: "share";
|
|
83
|
+
user: "user";
|
|
84
|
+
users: "users";
|
|
85
|
+
star: "star";
|
|
86
|
+
trophy: "trophy";
|
|
87
|
+
zap: "zap";
|
|
88
|
+
flame: "flame";
|
|
89
|
+
gift: "gift";
|
|
90
|
+
image: "image";
|
|
91
|
+
play: "play";
|
|
92
|
+
pause: "pause";
|
|
93
|
+
wallet: "wallet";
|
|
94
|
+
coins: "coins";
|
|
95
|
+
plus: "plus";
|
|
96
|
+
minus: "minus";
|
|
97
|
+
"refresh-cw": "refresh-cw";
|
|
98
|
+
bookmark: "bookmark";
|
|
99
|
+
"thumbs-up": "thumbs-up";
|
|
100
|
+
"thumbs-down": "thumbs-down";
|
|
101
|
+
"trending-up": "trending-up";
|
|
102
|
+
"trending-down": "trending-down";
|
|
103
|
+
}>;
|
|
104
|
+
color: z.ZodOptional<z.ZodEnum<{
|
|
105
|
+
gray: "gray";
|
|
106
|
+
blue: "blue";
|
|
107
|
+
red: "red";
|
|
108
|
+
amber: "amber";
|
|
109
|
+
green: "green";
|
|
110
|
+
teal: "teal";
|
|
111
|
+
purple: "purple";
|
|
112
|
+
pink: "pink";
|
|
113
|
+
accent: "accent";
|
|
114
|
+
}>>;
|
|
115
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
116
|
+
variant: z.ZodLiteral<"image">;
|
|
117
|
+
url: z.ZodString;
|
|
118
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
119
|
+
round: z.ZodOptional<z.ZodBoolean>;
|
|
120
|
+
}, z.core.$strict>], "variant">>;
|
|
121
|
+
}, z.core.$strict>;
|
|
12
122
|
export type ItemProps = z.infer<typeof itemProps>;
|
|
123
|
+
export type ItemMediaProps = z.infer<typeof itemMediaProps>;
|
package/dist/ui/item.js
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { PROGRESS_COLOR_VALUES } from "../colors.js";
|
|
3
|
+
import { ICON_NAMES } from "./icon.js";
|
|
2
4
|
export const ITEM_VARIANTS = ["default"];
|
|
5
|
+
export const ITEM_MEDIA_VARIANTS = ["icon", "image"];
|
|
3
6
|
export const ITEM_MAX_TITLE_CHARS = 100;
|
|
4
7
|
export const ITEM_MAX_DESCRIPTION_CHARS = 160;
|
|
5
|
-
export const
|
|
8
|
+
export const ITEM_MAX_MEDIA_ALT_CHARS = 120;
|
|
9
|
+
const itemIconMediaProps = z
|
|
10
|
+
.object({
|
|
11
|
+
variant: z.literal("icon"),
|
|
12
|
+
name: z.enum(ICON_NAMES),
|
|
13
|
+
color: z.enum(PROGRESS_COLOR_VALUES).optional(),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
const itemImageMediaProps = z
|
|
17
|
+
.object({
|
|
18
|
+
variant: z.literal("image"),
|
|
19
|
+
url: z.string(),
|
|
20
|
+
alt: z.string().max(ITEM_MAX_MEDIA_ALT_CHARS).optional(),
|
|
21
|
+
round: z.boolean().optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
export const itemMediaProps = z.discriminatedUnion("variant", [
|
|
25
|
+
itemIconMediaProps,
|
|
26
|
+
itemImageMediaProps,
|
|
27
|
+
]);
|
|
28
|
+
export const itemProps = z
|
|
29
|
+
.object({
|
|
6
30
|
title: z.string().min(1).max(ITEM_MAX_TITLE_CHARS),
|
|
7
31
|
description: z.string().max(ITEM_MAX_DESCRIPTION_CHARS).optional(),
|
|
8
32
|
variant: z.enum(ITEM_VARIANTS).optional(),
|
|
9
|
-
|
|
33
|
+
media: itemMediaProps.optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
package/dist/ui/stack.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const stackProps: z.ZodObject<{
|
|
|
20
20
|
between: "between";
|
|
21
21
|
around: "around";
|
|
22
22
|
}>>;
|
|
23
|
+
equalWidth: z.ZodOptional<z.ZodBoolean>;
|
|
23
24
|
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
|
|
24
25
|
}, z.core.$strip>;
|
|
25
26
|
export type StackProps = z.infer<typeof stackProps>;
|
package/dist/ui/stack.js
CHANGED
|
@@ -6,7 +6,9 @@ export const stackProps = z.object({
|
|
|
6
6
|
direction: z.enum(STACK_DIRECTIONS).optional(),
|
|
7
7
|
gap: z.enum(STACK_GAPS).optional(),
|
|
8
8
|
justify: z.enum(STACK_JUSTIFY).optional(),
|
|
9
|
-
/** Horizontal stacks only:
|
|
9
|
+
/** Horizontal stacks only: make direct children equal width. */
|
|
10
|
+
equalWidth: z.boolean().optional(),
|
|
11
|
+
/** Horizontal stacks only: legacy fixed equal-width column count (`2`–`6`). Prefer `equalWidth`. */
|
|
10
12
|
columns: z.union([
|
|
11
13
|
z.literal(2),
|
|
12
14
|
z.literal(3),
|
package/dist/validator.js
CHANGED
|
@@ -14,7 +14,10 @@ const URL_TARGET_ACTIONS = new Set([
|
|
|
14
14
|
*/
|
|
15
15
|
function isLoopback(url) {
|
|
16
16
|
const host = url.hostname;
|
|
17
|
-
return host === "localhost" ||
|
|
17
|
+
return (host === "localhost" ||
|
|
18
|
+
host === "127.0.0.1" ||
|
|
19
|
+
host === "::1" ||
|
|
20
|
+
host === "[::1]");
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* Validate a URL string: must be HTTPS (or HTTP on loopback for dev).
|
|
@@ -124,6 +127,21 @@ function validateUrls(elements) {
|
|
|
124
127
|
});
|
|
125
128
|
}
|
|
126
129
|
}
|
|
130
|
+
if (el.type === "item" &&
|
|
131
|
+
el.props?.media &&
|
|
132
|
+
typeof el.props.media === "object") {
|
|
133
|
+
const media = el.props.media;
|
|
134
|
+
if (media.variant === "image" && typeof media.url === "string") {
|
|
135
|
+
const error = validateUrl(media.url);
|
|
136
|
+
if (error) {
|
|
137
|
+
issues.push({
|
|
138
|
+
code: "custom",
|
|
139
|
+
message: error,
|
|
140
|
+
path: ["ui", "elements", id, "props", "media", "url"],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
127
145
|
// Validate action target URLs
|
|
128
146
|
if (el.on) {
|
|
129
147
|
for (const [event, binding] of Object.entries(el.on)) {
|
package/llms.txt
CHANGED
|
@@ -64,10 +64,11 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
|
|
|
64
64
|
- `aspect` (required): `"1:1"` | `"16:9"` | `"4:3"` | `"9:16"`
|
|
65
65
|
- `alt` (string, optional)
|
|
66
66
|
|
|
67
|
-
**item** — Content row
|
|
67
|
+
**item** — Content row matching shadcn Item: optional left media, content, and right-side actions slot.
|
|
68
68
|
- `title` (string, required, max 100)
|
|
69
69
|
- `description` (string, optional, max 160)
|
|
70
70
|
- `variant` (optional): `"default"`. Default: `"default"`
|
|
71
|
+
- `media` (optional): left-side media. Icon media: `{ "variant": "icon", "name": IconName, "color"?: PaletteColor }`. Image media: `{ "variant": "image", "url": string, "alt"?: string, "round"?: boolean }`.
|
|
71
72
|
- Children render in the actions slot (right side). Badges, buttons, and icons are all valid — but the item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.
|
|
72
73
|
|
|
73
74
|
**progress** — Horizontal progress bar.
|
|
@@ -97,6 +98,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
|
|
|
97
98
|
- `rows` (number, required, 2–16)
|
|
98
99
|
- `cells` (array, required): sparse list of `{ row, col, color?: PaletteColor | #rrggbb, content?: string, value?: string (1–30 chars) }`. When `value` is set on a cell, that string is what's written to `inputs[name]` on press/select; otherwise `"row,col"` is used. Use `value` for grids with meaningful labels (calendar days, alphabet letters, region codes) so handlers don't have to reverse-lookup. Use the row/col fallback for true coordinate grids (minesweeper, tic-tac-toe, pixel art).
|
|
99
100
|
- `gap` (optional): `"none"` (0px) | `"sm"` (1px) | `"md"` (2px) | `"lg"` (4px). Default: `"sm"`
|
|
101
|
+
- `cellAspectRatio` (optional): `"auto"` | `"square"`. Default: `"auto"`. Use `"square"` for game boards whose cells must stay square as snap width changes.
|
|
100
102
|
- `rowHeight` (number, optional, 8–64): pixel height per row. Default: 28. Grid height = rows × rowHeight
|
|
101
103
|
- `select` (optional): `"off"` | `"single"` | `"multiple"`. Default: `"off"`. With `select: "off"`, bind `on.press` for press-to-act (each press writes the cell's `value` or `"row,col"` to `inputs[name]` and fires the action). With `"single"` / `"multiple"`, presses accumulate selection state and pair with a separate submit `button` (multi-select joins values with `|`); `on.press` is ignored.
|
|
102
104
|
- Events: `press` — fires on cell press, only when `select: "off"`; `inputs[name]` is set to the pressed cell's `value` (or `"row,col"` fallback) before the bound action runs
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ButtonContentOrientation = "horizontal" | "vertical";
|
|
2
|
+
|
|
3
|
+
const MAX_HORIZONTAL_TOTAL_LENGTH: Record<number, number> = {
|
|
4
|
+
2: 20,
|
|
5
|
+
3: 15,
|
|
6
|
+
4: 11,
|
|
7
|
+
5: 8,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function displayLength(label: string): number {
|
|
11
|
+
return Array.from(label.trim().replace(/\s+/g, " ")).length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getButtonContentOrientation(
|
|
15
|
+
labels: readonly string[],
|
|
16
|
+
): ButtonContentOrientation {
|
|
17
|
+
const lengths = labels
|
|
18
|
+
.map((label) => displayLength(label))
|
|
19
|
+
.filter((length) => length > 0);
|
|
20
|
+
const count = lengths.length;
|
|
21
|
+
|
|
22
|
+
if (count <= 1) return "horizontal";
|
|
23
|
+
|
|
24
|
+
const maxTotalLength = MAX_HORIZONTAL_TOTAL_LENGTH[count] ?? 0;
|
|
25
|
+
|
|
26
|
+
if (maxTotalLength === 0) return "vertical";
|
|
27
|
+
|
|
28
|
+
const totalLength = lengths.reduce((sum, length) => sum + length, 0);
|
|
29
|
+
return totalLength <= maxTotalLength ? "horizontal" : "vertical";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldUseHorizontalButtonContent(
|
|
33
|
+
labels: readonly string[],
|
|
34
|
+
): boolean {
|
|
35
|
+
return getButtonContentOrientation(labels) === "horizontal";
|
|
36
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const SNAP_PAYLOAD_HEADER = "X-Snap-Payload" as const;
|
|
|
11
11
|
|
|
12
12
|
export const MEDIA_TYPE = "application/vnd.farcaster.snap+json" as const;
|
|
13
13
|
|
|
14
|
-
export const EFFECT_VALUES = ["confetti"] as const;
|
|
14
|
+
export const EFFECT_VALUES = ["confetti", "fireworks"] as const;
|
|
15
15
|
|
|
16
16
|
// ─── Pixel grid ────────────────────────────────────────
|
|
17
17
|
export const POST_GRID_TAP_KEY = "grid_tap" as const;
|
|
@@ -20,6 +20,7 @@ export const GRID_MAX_COLS = 32;
|
|
|
20
20
|
export const GRID_MIN_ROWS = 2;
|
|
21
21
|
export const GRID_MAX_ROWS = 16;
|
|
22
22
|
export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"] as const;
|
|
23
|
+
export const GRID_CELL_ASPECT_RATIO_VALUES = ["auto", "square"] as const;
|
|
23
24
|
|
|
24
25
|
// ─── Snap structural limits ───────────────────────────
|
|
25
26
|
export const MAX_ELEMENTS = 64;
|
|
@@ -60,16 +60,17 @@ export function SnapActionButton({
|
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
62
|
/**
|
|
63
|
-
* In a horizontal stack, `flex-
|
|
64
|
-
* In a vertical stack,
|
|
65
|
-
*
|
|
63
|
+
* In a horizontal stack, `flex-auto` lets the row fill available width while
|
|
64
|
+
* preserving content-proportional button widths. In a vertical stack, flex
|
|
65
|
+
* growth would silently stretch button height; stick to `w-full`.
|
|
66
66
|
*/
|
|
67
67
|
<div
|
|
68
68
|
className={
|
|
69
69
|
inHorizontalStack
|
|
70
|
-
? "
|
|
70
|
+
? "min-w-0 flex-auto"
|
|
71
71
|
: "w-full min-w-0"
|
|
72
72
|
}
|
|
73
|
+
style={inHorizontalStack ? { flex: "1 1 auto" } : undefined}
|
|
73
74
|
>
|
|
74
75
|
<Button
|
|
75
76
|
type="button"
|
|
@@ -27,6 +27,7 @@ export function SnapCellGrid({
|
|
|
27
27
|
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
28
28
|
const gapPx = gapMap[gap] ?? 1;
|
|
29
29
|
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
30
|
+
const squareCells = props.cellAspectRatio === "square";
|
|
30
31
|
|
|
31
32
|
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
32
33
|
const tapPath = `/inputs/${name}`;
|
|
@@ -78,7 +79,9 @@ export function SnapCellGrid({
|
|
|
78
79
|
|
|
79
80
|
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
80
81
|
const emptyCellBg =
|
|
81
|
-
colors.mode === "dark"
|
|
82
|
+
colors.mode === "dark"
|
|
83
|
+
? "rgba(255, 255, 255, 0.05)"
|
|
84
|
+
: "rgba(0, 0, 0, 0.05)";
|
|
82
85
|
|
|
83
86
|
const cellEls: ReactNode[] = [];
|
|
84
87
|
for (let r = 0; r < rows; r++) {
|
|
@@ -110,7 +113,8 @@ export function SnapCellGrid({
|
|
|
110
113
|
interactive ? "cursor-pointer select-none" : "cursor-default",
|
|
111
114
|
)}
|
|
112
115
|
style={{
|
|
113
|
-
height: rowHeight,
|
|
116
|
+
height: squareCells ? undefined : rowHeight,
|
|
117
|
+
aspectRatio: squareCells ? "1 / 1" : undefined,
|
|
114
118
|
background: bg,
|
|
115
119
|
color: textColor,
|
|
116
120
|
boxShadow: selected
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Children, type ReactNode, Fragment } from "react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
6
|
+
import { SnapItemGroupBorderProvider } from "./item-layout-context";
|
|
6
7
|
|
|
7
8
|
const GAP_MAP: Record<string, string> = {
|
|
8
9
|
none: "gap-0",
|
|
@@ -25,22 +26,23 @@ export function SnapItemGroup({
|
|
|
25
26
|
const colors = useSnapColors();
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
"flex flex-col",
|
|
31
|
-
border
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
<SnapItemGroupBorderProvider value={border}>
|
|
30
|
+
<div
|
|
31
|
+
className={cn("flex flex-col", border && "rounded-lg border", gap)}
|
|
32
|
+
style={border ? { borderColor: colors.border } : undefined}
|
|
33
|
+
>
|
|
34
|
+
{items.map((child, i) => (
|
|
35
|
+
<Fragment key={i}>
|
|
36
|
+
{separator && i > 0 && (
|
|
37
|
+
<div
|
|
38
|
+
className="h-px"
|
|
39
|
+
style={{ backgroundColor: colors.border }}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
{child}
|
|
43
|
+
</Fragment>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
</SnapItemGroupBorderProvider>
|
|
45
47
|
);
|
|
46
48
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
const SnapItemGroupBorderContext = createContext(false);
|
|
6
|
+
|
|
7
|
+
export const SnapItemGroupBorderProvider =
|
|
8
|
+
SnapItemGroupBorderContext.Provider;
|
|
9
|
+
|
|
10
|
+
export function useSnapItemGroupHasBorder() {
|
|
11
|
+
return useContext(SnapItemGroupBorderContext);
|
|
12
|
+
}
|