@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
|
@@ -14,6 +14,7 @@ export function SnapCellGrid({ element, emit, }) {
|
|
|
14
14
|
const rows = Number(props.rows ?? 2);
|
|
15
15
|
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
16
16
|
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
17
|
+
const squareCells = props.cellAspectRatio === "square";
|
|
17
18
|
const gap = String(props.gap ?? "sm");
|
|
18
19
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
19
20
|
const gapPx = gapMap[gap] ?? 1;
|
|
@@ -80,15 +81,36 @@ export function SnapCellGrid({ element, emit, }) {
|
|
|
80
81
|
const textColor = bgHex ? readableTextOnHex(bgHex) : colors.text;
|
|
81
82
|
const cellContent = cell?.content ? (_jsx(Text, { style: [styles.cellText, { color: textColor }], children: cell.content })) : null;
|
|
82
83
|
// Two-tone ring: outer View with contrasting border, inner View with inverse border
|
|
83
|
-
const cellView = selected ? (_jsx(View, { style: [
|
|
84
|
+
const cellView = selected ? (_jsx(View, { style: [
|
|
85
|
+
styles.cell,
|
|
86
|
+
squareCells ? styles.squareCell : { height: rowHeight },
|
|
87
|
+
{ borderWidth: 1, borderColor: ringOuter, borderRadius: 4 },
|
|
88
|
+
], children: _jsx(View, { style: [
|
|
84
89
|
styles.innerCell,
|
|
85
|
-
{
|
|
86
|
-
|
|
90
|
+
{
|
|
91
|
+
backgroundColor: bg,
|
|
92
|
+
borderWidth: 1,
|
|
93
|
+
borderColor: ringInner,
|
|
94
|
+
borderRadius: 3,
|
|
95
|
+
},
|
|
96
|
+
], children: cellContent }) })) : (_jsx(View, { style: [
|
|
97
|
+
styles.cell,
|
|
98
|
+
squareCells ? styles.squareCell : { height: rowHeight },
|
|
99
|
+
{ backgroundColor: bg },
|
|
100
|
+
], children: cellContent }));
|
|
87
101
|
rowCells.push(interactive ? (_jsx(Pressable, { onPress: () => handleTap(r, c), style: styles.cellWrap, children: cellView }, `${r}-${c}`)) : (_jsx(View, { style: styles.cellWrap, children: cellView }, `${r}-${c}`)));
|
|
88
102
|
}
|
|
89
103
|
rowEls.push(_jsx(View, { style: [styles.gridRow, { gap: gapPx }], children: rowCells }, r));
|
|
90
104
|
}
|
|
91
|
-
return (_jsx(View, { style: [
|
|
105
|
+
return (_jsx(View, { style: [
|
|
106
|
+
styles.wrap,
|
|
107
|
+
{
|
|
108
|
+
gap: gapPx,
|
|
109
|
+
backgroundColor: colors.muted,
|
|
110
|
+
padding: 4,
|
|
111
|
+
borderRadius: 8,
|
|
112
|
+
},
|
|
113
|
+
], children: rowEls }));
|
|
92
114
|
}
|
|
93
115
|
const styles = StyleSheet.create({
|
|
94
116
|
wrap: { width: "100%" },
|
|
@@ -99,6 +121,10 @@ const styles = StyleSheet.create({
|
|
|
99
121
|
alignItems: "center",
|
|
100
122
|
justifyContent: "center",
|
|
101
123
|
},
|
|
124
|
+
squareCell: {
|
|
125
|
+
aspectRatio: 1,
|
|
126
|
+
width: "100%",
|
|
127
|
+
},
|
|
102
128
|
innerCell: {
|
|
103
129
|
width: "100%",
|
|
104
130
|
height: "100%",
|
|
@@ -2,18 +2,28 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Children, Fragment } from "react";
|
|
3
3
|
import { StyleSheet, View } from "react-native";
|
|
4
4
|
import { useSnapTheme } from "../theme.js";
|
|
5
|
-
|
|
5
|
+
import { SnapItemGroupBorderProvider } from "./item-layout-context.js";
|
|
6
|
+
const GAP_MAP = {
|
|
7
|
+
none: 0,
|
|
8
|
+
sm: 4,
|
|
9
|
+
md: 8,
|
|
10
|
+
lg: 12,
|
|
11
|
+
};
|
|
6
12
|
export function SnapItemGroup({ element: { props }, children, }) {
|
|
7
13
|
const { colors } = useSnapTheme();
|
|
8
14
|
const border = Boolean(props.border);
|
|
9
15
|
const separator = Boolean(props.separator);
|
|
10
16
|
const gap = GAP_MAP[String(props.gap ?? "sm")] ?? 4;
|
|
11
17
|
const items = Children.toArray(children);
|
|
12
|
-
return (_jsx(View, { style: [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
return (_jsx(SnapItemGroupBorderProvider, { value: border, children: _jsx(View, { style: [
|
|
19
|
+
styles.group,
|
|
20
|
+
border && {
|
|
21
|
+
borderWidth: 1,
|
|
22
|
+
borderColor: colors.border,
|
|
23
|
+
borderRadius: 12,
|
|
24
|
+
},
|
|
25
|
+
{ gap },
|
|
26
|
+
], children: items.map((child, i) => (_jsxs(Fragment, { children: [separator && i > 0 && (_jsx(View, { style: { height: 1, backgroundColor: colors.border + "80" } })), child] }, i))) }) }));
|
|
17
27
|
}
|
|
18
28
|
const styles = StyleSheet.create({
|
|
19
29
|
group: {
|
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Image } from "expo-image";
|
|
2
3
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
4
|
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
4
5
|
import { useSnapTheme } from "../theme.js";
|
|
6
|
+
import { useSnapPalette } from "../use-snap-palette.js";
|
|
7
|
+
import { useSnapItemGroupHasBorder } from "./item-layout-context.js";
|
|
8
|
+
import { ICON_MAP } from "./snap-icon.js";
|
|
9
|
+
function parseItemMedia(value) {
|
|
10
|
+
if (!value || typeof value !== "object")
|
|
11
|
+
return undefined;
|
|
12
|
+
const media = value;
|
|
13
|
+
if (media.variant === "icon" && typeof media.name === "string") {
|
|
14
|
+
return {
|
|
15
|
+
variant: "icon",
|
|
16
|
+
name: media.name,
|
|
17
|
+
color: typeof media.color === "string" ? media.color : undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (media.variant === "image" && typeof media.url === "string") {
|
|
21
|
+
return {
|
|
22
|
+
variant: "image",
|
|
23
|
+
url: media.url,
|
|
24
|
+
alt: typeof media.alt === "string" ? media.alt : undefined,
|
|
25
|
+
round: typeof media.round === "boolean" ? media.round : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
5
30
|
export function SnapItem({ element: { props }, children, }) {
|
|
6
31
|
const { colors } = useSnapTheme();
|
|
32
|
+
const { accentHex, hex } = useSnapPalette();
|
|
7
33
|
const title = String(props.title ?? "");
|
|
8
|
-
const description = props.description
|
|
9
|
-
|
|
10
|
-
|
|
34
|
+
const description = props.description ? String(props.description) : undefined;
|
|
35
|
+
const media = parseItemMedia(props.media);
|
|
36
|
+
const inBorderedGroup = useSnapItemGroupHasBorder();
|
|
11
37
|
/** Match web `Item className="flex-1"`: row peers must share width or title/description collapse. */
|
|
12
38
|
const rowPeer = useSnapStackDirection() === "horizontal";
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
const MediaIcon = media?.variant === "icon" ? ICON_MAP[media.name] : undefined;
|
|
40
|
+
const mediaColor = media?.variant === "icon" && media.color && media.color !== "accent"
|
|
41
|
+
? hex(media.color)
|
|
42
|
+
: accentHex;
|
|
43
|
+
const containerVariant = {
|
|
44
|
+
paddingVertical: 6,
|
|
45
|
+
paddingHorizontal: inBorderedGroup ? 8 : 0,
|
|
46
|
+
columnGap: 8,
|
|
47
|
+
};
|
|
48
|
+
return (_jsxs(View, { style: [styles.container, containerVariant, rowPeer && styles.rowPeer], children: [media?.variant === "icon" && MediaIcon ? (_jsx(View, { style: styles.iconMedia, children: _jsx(MediaIcon, { size: 20, color: mediaColor }) })) : null, media?.variant === "image" ? (_jsx(View, { style: [styles.imageMedia, media.round && styles.roundImage], children: _jsx(Image, { source: { uri: media.url }, style: StyleSheet.absoluteFill, contentFit: "cover", accessibilityLabel: media.alt || undefined }) })) : null, _jsxs(View, { style: styles.content, children: [title ? (_jsx(Text, { style: [styles.title, { color: colors.text }], children: title })) : null, 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] }));
|
|
19
49
|
}
|
|
20
50
|
const styles = StyleSheet.create({
|
|
21
51
|
container: {
|
|
@@ -29,19 +59,32 @@ const styles = StyleSheet.create({
|
|
|
29
59
|
content: {
|
|
30
60
|
flex: 1,
|
|
31
61
|
},
|
|
62
|
+
iconMedia: {
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
},
|
|
66
|
+
imageMedia: {
|
|
67
|
+
width: 40,
|
|
68
|
+
height: 40,
|
|
69
|
+
borderRadius: 6,
|
|
70
|
+
overflow: "hidden",
|
|
71
|
+
backgroundColor: "#f3f4f6",
|
|
72
|
+
},
|
|
73
|
+
roundImage: {
|
|
74
|
+
borderRadius: 9999,
|
|
75
|
+
},
|
|
32
76
|
title: {
|
|
33
77
|
fontSize: 15,
|
|
34
78
|
lineHeight: 20,
|
|
35
79
|
fontWeight: "500",
|
|
36
80
|
},
|
|
37
81
|
description: {
|
|
38
|
-
fontSize:
|
|
39
|
-
lineHeight:
|
|
40
|
-
marginTop: 1,
|
|
82
|
+
fontSize: 12,
|
|
83
|
+
lineHeight: 16,
|
|
41
84
|
},
|
|
42
85
|
actions: {
|
|
43
86
|
marginLeft: "auto",
|
|
44
|
-
paddingLeft:
|
|
87
|
+
paddingLeft: 8,
|
|
45
88
|
flexDirection: "row",
|
|
46
89
|
alignItems: "center",
|
|
47
90
|
flexShrink: 0,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Children } from "react";
|
|
3
3
|
import { StyleSheet, View } from "react-native";
|
|
4
|
-
import { countRenderableChildren, defaultHorizontalGapSize,
|
|
4
|
+
import { childrenShouldUseHorizontalButtonLayout, childrenAreAllButtons, countRenderableChildren, defaultHorizontalGapSize, } from "../../stack-horizontal-utils.js";
|
|
5
5
|
import { SnapStackDirectionProvider, useSnapStackDirection, } from "../stack-direction-context.js";
|
|
6
6
|
const VGAP = {
|
|
7
7
|
none: 0,
|
|
8
|
-
sm:
|
|
8
|
+
sm: 4,
|
|
9
9
|
md: 16,
|
|
10
10
|
lg: 24,
|
|
11
11
|
};
|
|
@@ -22,62 +22,60 @@ const JUSTIFY = {
|
|
|
22
22
|
between: "space-between",
|
|
23
23
|
around: "space-around",
|
|
24
24
|
};
|
|
25
|
-
/** Equal-width cells for explicit `
|
|
25
|
+
/** Equal-width cells for explicit `equalWidth` / `columns` props. */
|
|
26
26
|
function wrapEqualColumnCells(children) {
|
|
27
27
|
const cells = Children.toArray(children).filter((c) => c != null && c !== false);
|
|
28
28
|
return cells.map((child, i) => (_jsx(View, { style: styles.equalColumnCell, children: child }, i)));
|
|
29
29
|
}
|
|
30
30
|
export function SnapStack({ element: { props }, children, }) {
|
|
31
31
|
const parentDirection = useSnapStackDirection();
|
|
32
|
-
const
|
|
32
|
+
const buttonContentUsesHorizontal = childrenShouldUseHorizontalButtonLayout(children);
|
|
33
|
+
const direction = buttonContentUsesHorizontal === undefined
|
|
34
|
+
? String(props.direction ?? "vertical")
|
|
35
|
+
: buttonContentUsesHorizontal
|
|
36
|
+
? "horizontal"
|
|
37
|
+
: "vertical";
|
|
33
38
|
const rawGap = props.gap;
|
|
34
39
|
const isHorizontal = direction === "horizontal";
|
|
35
40
|
const gapMap = isHorizontal ? HGAP : VGAP;
|
|
36
|
-
const
|
|
37
|
-
const buttonRowCount = buttonRowGrid
|
|
38
|
-
? countRenderableChildren(children)
|
|
39
|
-
: 0;
|
|
41
|
+
const allChildrenAreButtons = childrenAreAllButtons(children);
|
|
40
42
|
const columnsRaw = props.columns;
|
|
43
|
+
const equalWidth = props.equalWidth === true;
|
|
41
44
|
const columns = typeof columnsRaw === "number" &&
|
|
42
45
|
columnsRaw >= 2 &&
|
|
43
46
|
columnsRaw <= 6 &&
|
|
44
47
|
Number.isInteger(columnsRaw)
|
|
45
48
|
? columnsRaw
|
|
46
49
|
: undefined;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const equalWidthColumnCount = columns ?? (equalWidth ? countRenderableChildren(children) : undefined);
|
|
51
|
+
const explicitEqualWidth = isHorizontal &&
|
|
52
|
+
equalWidthColumnCount !== undefined &&
|
|
53
|
+
equalWidthColumnCount >= 1 &&
|
|
54
|
+
equalWidthColumnCount <= 6;
|
|
55
|
+
// Button-only stacks always default to sm; mixed horizontal stacks scale by child count.
|
|
56
|
+
// Vertical non-button stacks default to md.
|
|
57
|
+
const horizontalChildCount = isHorizontal
|
|
58
|
+
? (explicitEqualWidth
|
|
59
|
+
? equalWidthColumnCount
|
|
60
|
+
: countRenderableChildren(children))
|
|
54
61
|
: undefined;
|
|
55
62
|
const gap = typeof rawGap === "number"
|
|
56
63
|
? rawGap
|
|
57
64
|
: typeof rawGap === "string" && rawGap in gapMap
|
|
58
65
|
? gapMap[rawGap]
|
|
59
|
-
:
|
|
60
|
-
? gapMap
|
|
61
|
-
:
|
|
62
|
-
|
|
66
|
+
: allChildrenAreButtons
|
|
67
|
+
? gapMap.sm
|
|
68
|
+
: isHorizontal
|
|
69
|
+
? gapMap[defaultHorizontalGapSize(horizontalChildCount)]
|
|
70
|
+
: VGAP.md;
|
|
63
71
|
const justify = props.justify &&
|
|
64
|
-
(!isHorizontal ||
|
|
72
|
+
(!isHorizontal || !explicitEqualWidth)
|
|
65
73
|
? JUSTIFY[String(props.justify)]
|
|
66
74
|
: undefined;
|
|
67
75
|
const isRowChild = parentDirection === "horizontal";
|
|
68
|
-
const packedHorizontal = isHorizontal &&
|
|
69
|
-
((buttonRowGrid &&
|
|
70
|
-
buttonRowCount >= 1 &&
|
|
71
|
-
buttonRowCount <= 6) ||
|
|
72
|
-
explicitColumnGrid);
|
|
76
|
+
const packedHorizontal = isHorizontal && explicitEqualWidth;
|
|
73
77
|
let horizontalBody = children;
|
|
74
|
-
if (isHorizontal &&
|
|
75
|
-
buttonRowGrid &&
|
|
76
|
-
buttonRowCount >= 1 &&
|
|
77
|
-
buttonRowCount <= 6) {
|
|
78
|
-
horizontalBody = wrapEqualColumnCells(children);
|
|
79
|
-
}
|
|
80
|
-
else if (isHorizontal && explicitColumnGrid && columns !== undefined) {
|
|
78
|
+
if (isHorizontal && explicitEqualWidth) {
|
|
81
79
|
horizontalBody = wrapEqualColumnCells(children);
|
|
82
80
|
}
|
|
83
81
|
return (_jsx(SnapStackDirectionProvider, { direction: isHorizontal ? "horizontal" : "vertical", children: _jsx(View, { style: [
|
|
@@ -117,7 +115,7 @@ const styles = StyleSheet.create({
|
|
|
117
115
|
width: "100%",
|
|
118
116
|
minWidth: 0,
|
|
119
117
|
},
|
|
120
|
-
/** Single row for packed equal-width cells
|
|
118
|
+
/** Single row for packed equal-width cells from explicit equal-width layout. */
|
|
121
119
|
horizontalPacked: {
|
|
122
120
|
flexDirection: "row",
|
|
123
121
|
flexWrap: "nowrap",
|
|
@@ -130,5 +128,6 @@ const styles = StyleSheet.create({
|
|
|
130
128
|
flexShrink: 1,
|
|
131
129
|
flexBasis: 0,
|
|
132
130
|
minWidth: 0,
|
|
131
|
+
alignSelf: "stretch",
|
|
133
132
|
},
|
|
134
133
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useStateStore } from "@json-render/react-native";
|
|
3
3
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
|
+
import { shouldUseHorizontalButtonContent } from "../../button-orientation-utils.js";
|
|
4
5
|
import { useSnapTheme } from "../theme.js";
|
|
5
6
|
export function SnapToggleGroup({ element: { props }, }) {
|
|
6
7
|
const { get, set } = useStateStore();
|
|
@@ -9,7 +10,6 @@ export function SnapToggleGroup({ element: { props }, }) {
|
|
|
9
10
|
const path = `/inputs/${name}`;
|
|
10
11
|
const label = props.label ? String(props.label) : undefined;
|
|
11
12
|
const isMultiple = Boolean(props.multiple);
|
|
12
|
-
const orientation = String(props.orientation ?? "horizontal");
|
|
13
13
|
const options = Array.isArray(props.options)
|
|
14
14
|
? props.options
|
|
15
15
|
: [];
|
|
@@ -32,7 +32,7 @@ export function SnapToggleGroup({ element: { props }, }) {
|
|
|
32
32
|
}
|
|
33
33
|
return [];
|
|
34
34
|
})();
|
|
35
|
-
const isVertical =
|
|
35
|
+
const isVertical = !shouldUseHorizontalButtonContent(options);
|
|
36
36
|
const handlePress = (opt) => {
|
|
37
37
|
if (isMultiple) {
|
|
38
38
|
const next = selected.includes(opt)
|
|
@@ -64,7 +64,7 @@ export function SnapToggleGroup({ element: { props }, }) {
|
|
|
64
64
|
], onPress: () => handlePress(opt), children: _jsx(Text, { style: [
|
|
65
65
|
styles.optionText,
|
|
66
66
|
{ color: colors.text },
|
|
67
|
-
], children: opt }) }, index));
|
|
67
|
+
], children: opt }) }, `${opt}-${index}`));
|
|
68
68
|
}) })] }));
|
|
69
69
|
}
|
|
70
70
|
const styles = StyleSheet.create({
|
|
@@ -89,7 +89,8 @@ const styles = StyleSheet.create({
|
|
|
89
89
|
justifyContent: "center",
|
|
90
90
|
},
|
|
91
91
|
optionHorizontal: {
|
|
92
|
-
|
|
92
|
+
flexGrow: 1,
|
|
93
|
+
flexShrink: 1,
|
|
93
94
|
},
|
|
94
95
|
optionText: {
|
|
95
96
|
fontSize: 13,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function FireworksOverlay(): import("react").JSX.Element;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Animated, StyleSheet, View, useWindowDimensions } from "react-native";
|
|
4
|
+
const FIREWORK_COLORS = [
|
|
5
|
+
"#FFD700",
|
|
6
|
+
"#FF6B6B",
|
|
7
|
+
"#4ECDC4",
|
|
8
|
+
"#C4A7E7",
|
|
9
|
+
"#F6C177",
|
|
10
|
+
"#EBBCBA",
|
|
11
|
+
"#9CCFD8",
|
|
12
|
+
"#fff",
|
|
13
|
+
];
|
|
14
|
+
const BURST_COUNT = 5;
|
|
15
|
+
const PARTICLE_COUNT = 24;
|
|
16
|
+
function FireworkBurst({ burst }) {
|
|
17
|
+
const flashAnim = useRef(new Animated.Value(0)).current;
|
|
18
|
+
const burstAnim = useRef(new Animated.Value(0)).current;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const composite = Animated.parallel([
|
|
21
|
+
Animated.timing(flashAnim, {
|
|
22
|
+
toValue: 1,
|
|
23
|
+
duration: 400,
|
|
24
|
+
useNativeDriver: true,
|
|
25
|
+
}),
|
|
26
|
+
Animated.timing(burstAnim, {
|
|
27
|
+
toValue: 1,
|
|
28
|
+
duration: 1000,
|
|
29
|
+
useNativeDriver: true,
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
composite.start();
|
|
33
|
+
return () => composite.stop();
|
|
34
|
+
}, [flashAnim, burstAnim]);
|
|
35
|
+
const flashOpacity = flashAnim.interpolate({
|
|
36
|
+
inputRange: [0, 0.25, 1],
|
|
37
|
+
outputRange: [0, 1, 0],
|
|
38
|
+
});
|
|
39
|
+
const flashScale = flashAnim.interpolate({
|
|
40
|
+
inputRange: [0, 0.25, 1],
|
|
41
|
+
outputRange: [0, 2.5, 5],
|
|
42
|
+
});
|
|
43
|
+
return (_jsxs(_Fragment, { children: [_jsx(Animated.View, { style: [
|
|
44
|
+
styles.flash,
|
|
45
|
+
{
|
|
46
|
+
left: burst.x - 6,
|
|
47
|
+
top: burst.y - 6,
|
|
48
|
+
opacity: flashOpacity,
|
|
49
|
+
transform: [{ scale: flashScale }],
|
|
50
|
+
},
|
|
51
|
+
] }), burst.particles.map((p) => {
|
|
52
|
+
const opacity = burstAnim.interpolate({
|
|
53
|
+
inputRange: [0, 0.65, 1],
|
|
54
|
+
outputRange: [1, 1, 0],
|
|
55
|
+
});
|
|
56
|
+
const translateX = burstAnim.interpolate({
|
|
57
|
+
inputRange: [0, 1],
|
|
58
|
+
outputRange: [0, p.vx],
|
|
59
|
+
});
|
|
60
|
+
const translateY = burstAnim.interpolate({
|
|
61
|
+
inputRange: [0, 1],
|
|
62
|
+
outputRange: [0, p.vy],
|
|
63
|
+
});
|
|
64
|
+
const scale = burstAnim.interpolate({
|
|
65
|
+
inputRange: [0, 1],
|
|
66
|
+
outputRange: [1, 0],
|
|
67
|
+
});
|
|
68
|
+
return (_jsx(Animated.View, { style: [
|
|
69
|
+
styles.particle,
|
|
70
|
+
{
|
|
71
|
+
left: burst.x - p.size / 2,
|
|
72
|
+
top: burst.y - p.size / 2,
|
|
73
|
+
width: p.size,
|
|
74
|
+
height: p.size,
|
|
75
|
+
backgroundColor: p.color,
|
|
76
|
+
opacity,
|
|
77
|
+
transform: [{ translateX }, { translateY }, { scale }],
|
|
78
|
+
},
|
|
79
|
+
] }, p.id));
|
|
80
|
+
})] }));
|
|
81
|
+
}
|
|
82
|
+
export function FireworksOverlay() {
|
|
83
|
+
const { width, height } = useWindowDimensions();
|
|
84
|
+
const bursts = useMemo(() => Array.from({ length: BURST_COUNT }, (_, b) => ({
|
|
85
|
+
id: b,
|
|
86
|
+
x: (0.15 + Math.random() * 0.7) * width,
|
|
87
|
+
y: (0.1 + Math.random() * 0.5) * height,
|
|
88
|
+
delay: b * 500 + Math.random() * 200,
|
|
89
|
+
particles: Array.from({ length: PARTICLE_COUNT }, (_, p) => {
|
|
90
|
+
const angle = (p / PARTICLE_COUNT) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
91
|
+
const dist = 55 + Math.random() * 60;
|
|
92
|
+
return {
|
|
93
|
+
id: p,
|
|
94
|
+
vx: Math.cos(angle) * dist,
|
|
95
|
+
vy: Math.sin(angle) * dist,
|
|
96
|
+
color: FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)],
|
|
97
|
+
size: 3 + Math.random() * 3,
|
|
98
|
+
};
|
|
99
|
+
}),
|
|
100
|
+
})),
|
|
101
|
+
// stable on mount
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
[]);
|
|
104
|
+
const [mountedBursts, setMountedBursts] = useState([]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const timers = bursts.map((burst, b) => setTimeout(() => {
|
|
107
|
+
setMountedBursts((prev) => [...prev, b]);
|
|
108
|
+
}, burst.delay));
|
|
109
|
+
return () => timers.forEach(clearTimeout);
|
|
110
|
+
}, [bursts]);
|
|
111
|
+
return (_jsx(View, { style: StyleSheet.absoluteFill, pointerEvents: "none", children: mountedBursts.map((b) => (_jsx(FireworkBurst, { burst: bursts[b] }, b))) }));
|
|
112
|
+
}
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
flash: {
|
|
115
|
+
position: "absolute",
|
|
116
|
+
width: 12,
|
|
117
|
+
height: 12,
|
|
118
|
+
borderRadius: 6,
|
|
119
|
+
backgroundColor: "#fff",
|
|
120
|
+
},
|
|
121
|
+
particle: {
|
|
122
|
+
position: "absolute",
|
|
123
|
+
borderRadius: 999,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
4
4
|
import { ConfettiOverlay } from "./confetti-overlay.js";
|
|
5
|
+
import { FireworksOverlay } from "./fireworks-overlay.js";
|
|
5
6
|
import { useSnapTheme } from "./theme.js";
|
|
6
7
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
7
8
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
@@ -81,11 +82,15 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
81
82
|
setPageKey((k) => k + 1);
|
|
82
83
|
}, [spec]);
|
|
83
84
|
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
85
|
+
const showFireworks = snap.effects?.includes("fireworks") ?? false;
|
|
84
86
|
const [confettiKey, setConfettiKey] = useState(0);
|
|
87
|
+
const [fireworksKey, setFireworksKey] = useState(0);
|
|
85
88
|
useEffect(() => {
|
|
86
89
|
if (showConfetti)
|
|
87
90
|
setConfettiKey((k) => k + 1);
|
|
88
|
-
|
|
91
|
+
if (showFireworks)
|
|
92
|
+
setFireworksKey((k) => k + 1);
|
|
93
|
+
}, [showConfetti, showFireworks, snap]);
|
|
89
94
|
const handlersRef = useRef(handlers);
|
|
90
95
|
handlersRef.current = handlers;
|
|
91
96
|
const handleAction = useCallback((name, params) => {
|
|
@@ -147,7 +152,7 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
147
152
|
: loadingOverlay
|
|
148
153
|
: null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
149
154
|
applyStatePaths(stateRef.current, changes);
|
|
150
|
-
}, onAction: handleAction }, pageKey), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey)] }));
|
|
155
|
+
}, onAction: handleAction }, pageKey), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), showFireworks && _jsx(FireworksOverlay, {}, fireworksKey)] }));
|
|
151
156
|
}
|
|
152
157
|
export function SnapLoadingOverlay({ appearance, accentHex, }) {
|
|
153
158
|
return (_jsx(View, { style: [
|
package/dist/schemas.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
export declare function
|
|
3
|
-
|
|
2
|
+
export declare function childrenAreAllButtons(children: ReactNode): boolean;
|
|
3
|
+
export declare function getButtonChildLabels(children: ReactNode): string[] | undefined;
|
|
4
|
+
export declare function childrenShouldUseHorizontalButtonLayout(children: ReactNode): boolean | undefined;
|
|
5
|
+
/** Direct snap catalog children under a stack (used for horizontal gap defaults). */
|
|
4
6
|
export declare function countRenderableChildren(children: ReactNode): number;
|
|
5
7
|
/**
|
|
6
|
-
* Default horizontal stack gap as a t-shirt size, chosen by
|
|
7
|
-
* 2
|
|
8
|
+
* Default horizontal stack gap as a t-shirt size, chosen by direct child count:
|
|
9
|
+
* 2 children → lg, 3 children → md, 4+ children → sm. Unknown count falls back to md.
|
|
8
10
|
* Tighter gaps for denser layouts; authors can always override via the `gap` prop.
|
|
9
11
|
*/
|
|
10
|
-
export declare function defaultHorizontalGapSize(
|
|
12
|
+
export declare function defaultHorizontalGapSize(childCount: number | undefined): "sm" | "md" | "lg";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Children, isValidElement } from "react";
|
|
2
|
+
import { shouldUseHorizontalButtonContent } from "./button-orientation-utils.js";
|
|
2
3
|
/**
|
|
3
4
|
* True when every rendered child comes from a catalog `button` element.
|
|
4
5
|
* json-render passes `{ element: { type, props, ... } }` into each catalog component.
|
|
@@ -10,34 +11,43 @@ function isRenderableChild(c) {
|
|
|
10
11
|
return false;
|
|
11
12
|
return true;
|
|
12
13
|
}
|
|
13
|
-
export function
|
|
14
|
+
export function childrenAreAllButtons(children) {
|
|
15
|
+
return getButtonChildLabels(children) !== undefined;
|
|
16
|
+
}
|
|
17
|
+
export function getButtonChildLabels(children) {
|
|
14
18
|
const items = Children.toArray(children).filter(isRenderableChild);
|
|
15
19
|
if (items.length === 0)
|
|
16
|
-
return
|
|
20
|
+
return undefined;
|
|
21
|
+
const labels = [];
|
|
17
22
|
for (const child of items) {
|
|
18
23
|
if (!isValidElement(child))
|
|
19
|
-
return
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
24
|
+
return undefined;
|
|
25
|
+
const element = child.props.element;
|
|
26
|
+
if (element?.type !== "button")
|
|
27
|
+
return undefined;
|
|
28
|
+
labels.push(String(element.props?.label ?? ""));
|
|
23
29
|
}
|
|
24
|
-
return
|
|
30
|
+
return labels;
|
|
31
|
+
}
|
|
32
|
+
export function childrenShouldUseHorizontalButtonLayout(children) {
|
|
33
|
+
const labels = getButtonChildLabels(children);
|
|
34
|
+
return labels ? shouldUseHorizontalButtonContent(labels) : undefined;
|
|
25
35
|
}
|
|
26
|
-
/** Direct snap catalog children under a stack (used for
|
|
36
|
+
/** Direct snap catalog children under a stack (used for horizontal gap defaults). */
|
|
27
37
|
export function countRenderableChildren(children) {
|
|
28
38
|
return Children.toArray(children).filter(isRenderableChild).length;
|
|
29
39
|
}
|
|
30
40
|
/**
|
|
31
|
-
* Default horizontal stack gap as a t-shirt size, chosen by
|
|
32
|
-
* 2
|
|
41
|
+
* Default horizontal stack gap as a t-shirt size, chosen by direct child count:
|
|
42
|
+
* 2 children → lg, 3 children → md, 4+ children → sm. Unknown count falls back to md.
|
|
33
43
|
* Tighter gaps for denser layouts; authors can always override via the `gap` prop.
|
|
34
44
|
*/
|
|
35
|
-
export function defaultHorizontalGapSize(
|
|
36
|
-
if (
|
|
45
|
+
export function defaultHorizontalGapSize(childCount) {
|
|
46
|
+
if (childCount === undefined)
|
|
37
47
|
return "md";
|
|
38
|
-
if (
|
|
48
|
+
if (childCount <= 2)
|
|
39
49
|
return "lg";
|
|
40
|
-
if (
|
|
50
|
+
if (childCount === 3)
|
|
41
51
|
return "md";
|
|
42
52
|
return "sm";
|
|
43
53
|
}
|