@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
|
@@ -6,10 +6,50 @@ import {
|
|
|
6
6
|
ItemTitle,
|
|
7
7
|
ItemDescription,
|
|
8
8
|
ItemActions,
|
|
9
|
+
ItemMedia,
|
|
9
10
|
} from "@neynar/ui/item";
|
|
10
11
|
import { cn } from "@neynar/ui/utils";
|
|
11
12
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
12
13
|
import { useSnapStackDirection } from "../stack-direction-context";
|
|
14
|
+
import { ICON_MAP } from "./icon";
|
|
15
|
+
import { useSnapItemGroupHasBorder } from "./item-layout-context";
|
|
16
|
+
|
|
17
|
+
type ItemMediaConfig =
|
|
18
|
+
| {
|
|
19
|
+
variant: "icon";
|
|
20
|
+
name: string;
|
|
21
|
+
color?: string;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
variant: "image";
|
|
25
|
+
url: string;
|
|
26
|
+
alt?: string;
|
|
27
|
+
round?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function parseItemMedia(value: unknown): ItemMediaConfig | undefined {
|
|
31
|
+
if (!value || typeof value !== "object") return undefined;
|
|
32
|
+
|
|
33
|
+
const media = value as Record<string, unknown>;
|
|
34
|
+
if (media.variant === "icon" && typeof media.name === "string") {
|
|
35
|
+
return {
|
|
36
|
+
variant: "icon",
|
|
37
|
+
name: media.name,
|
|
38
|
+
color: typeof media.color === "string" ? media.color : undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (media.variant === "image" && typeof media.url === "string") {
|
|
43
|
+
return {
|
|
44
|
+
variant: "image",
|
|
45
|
+
url: media.url,
|
|
46
|
+
alt: typeof media.alt === "string" ? media.alt : undefined,
|
|
47
|
+
round: typeof media.round === "boolean" ? media.round : undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
13
53
|
|
|
14
54
|
export function SnapItem({
|
|
15
55
|
element: { props, children: childIds },
|
|
@@ -20,26 +60,79 @@ export function SnapItem({
|
|
|
20
60
|
}) {
|
|
21
61
|
const title = String(props.title ?? "");
|
|
22
62
|
const description = props.description ? String(props.description) : undefined;
|
|
63
|
+
const media = parseItemMedia(props.media);
|
|
23
64
|
const colors = useSnapColors();
|
|
65
|
+
const inBorderedGroup = useSnapItemGroupHasBorder();
|
|
24
66
|
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
67
|
+
const MediaIcon =
|
|
68
|
+
media?.variant === "icon" ? ICON_MAP[media.name] : undefined;
|
|
25
69
|
|
|
26
70
|
return (
|
|
27
71
|
<Item
|
|
28
72
|
className={cn(
|
|
29
|
-
"py-1.5
|
|
73
|
+
"gap-2 py-1.5",
|
|
74
|
+
inBorderedGroup ? "px-2" : "px-0",
|
|
30
75
|
/** Horizontal: share width with peers. Vertical: don't fill column height. */
|
|
31
76
|
inHorizontalStack && "flex-1",
|
|
32
77
|
)}
|
|
78
|
+
style={{
|
|
79
|
+
columnGap: 8,
|
|
80
|
+
paddingInline: inBorderedGroup ? 8 : 0,
|
|
81
|
+
}}
|
|
33
82
|
>
|
|
34
|
-
|
|
83
|
+
{media?.variant === "icon" && MediaIcon && (
|
|
84
|
+
<ItemMedia
|
|
85
|
+
variant="icon"
|
|
86
|
+
className="self-center translate-y-0"
|
|
87
|
+
style={{ alignSelf: "center", transform: "none" }}
|
|
88
|
+
>
|
|
89
|
+
<MediaIcon
|
|
90
|
+
size={20}
|
|
91
|
+
style={{ color: colors.colorHex(media.color) }}
|
|
92
|
+
/>
|
|
93
|
+
</ItemMedia>
|
|
94
|
+
)}
|
|
95
|
+
{media?.variant === "image" && (
|
|
96
|
+
<ItemMedia
|
|
97
|
+
variant="image"
|
|
98
|
+
className="self-center translate-y-0"
|
|
99
|
+
style={{
|
|
100
|
+
alignSelf: "center",
|
|
101
|
+
borderRadius: media.round ? "9999px" : undefined,
|
|
102
|
+
transform: "none",
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
106
|
+
<img
|
|
107
|
+
src={media.url}
|
|
108
|
+
alt={media.alt ?? ""}
|
|
109
|
+
className="size-full object-cover"
|
|
110
|
+
/>
|
|
111
|
+
</ItemMedia>
|
|
112
|
+
)}
|
|
113
|
+
<ItemContent className="gap-0">
|
|
35
114
|
<ItemTitle style={{ color: colors.text }}>{title}</ItemTitle>
|
|
36
115
|
{description && (
|
|
37
|
-
<ItemDescription
|
|
116
|
+
<ItemDescription
|
|
117
|
+
className="mt-0 text-xs leading-snug"
|
|
118
|
+
style={{
|
|
119
|
+
color: colors.textMuted,
|
|
120
|
+
fontSize: 12,
|
|
121
|
+
lineHeight: "16px",
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
38
124
|
{description}
|
|
39
125
|
</ItemDescription>
|
|
40
126
|
)}
|
|
41
127
|
</ItemContent>
|
|
42
|
-
{childIds && childIds.length > 0 &&
|
|
128
|
+
{childIds && childIds.length > 0 && (
|
|
129
|
+
<ItemActions
|
|
130
|
+
className="gap-1.5 self-center"
|
|
131
|
+
style={{ alignSelf: "center", columnGap: 6 }}
|
|
132
|
+
>
|
|
133
|
+
{children}
|
|
134
|
+
</ItemActions>
|
|
135
|
+
)}
|
|
43
136
|
</Item>
|
|
44
137
|
);
|
|
45
138
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { ReactNode } from "react";
|
|
3
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import {
|
|
6
|
+
childrenShouldUseHorizontalButtonLayout,
|
|
7
|
+
childrenAreAllButtons,
|
|
6
8
|
countRenderableChildren,
|
|
7
9
|
defaultHorizontalGapSize,
|
|
8
|
-
horizontalChildrenAreAllButtons,
|
|
9
10
|
} from "../../stack-horizontal-utils.js";
|
|
10
11
|
import {
|
|
11
12
|
SnapStackDirectionProvider,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
|
|
15
16
|
const VGAP: Record<string, string> = {
|
|
16
17
|
none: "gap-0",
|
|
17
|
-
sm: "gap-
|
|
18
|
+
sm: "gap-1",
|
|
18
19
|
md: "gap-4",
|
|
19
20
|
lg: "gap-6",
|
|
20
21
|
};
|
|
@@ -34,14 +35,14 @@ const JUSTIFY_FLEX: Record<string, string> = {
|
|
|
34
35
|
around: "justify-around",
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
/** Equal
|
|
38
|
+
/** Equal-width cell count for explicit `equalWidth` / `columns` props. */
|
|
38
39
|
const COLUMN_GRID_CLASS: Record<number, string> = {
|
|
39
|
-
1: "
|
|
40
|
-
2: "
|
|
41
|
-
3: "
|
|
42
|
-
4: "
|
|
43
|
-
5: "
|
|
44
|
-
6: "
|
|
40
|
+
1: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
41
|
+
2: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
42
|
+
3: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
43
|
+
4: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
44
|
+
5: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
45
|
+
6: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export function SnapStack({
|
|
@@ -52,17 +53,21 @@ export function SnapStack({
|
|
|
52
53
|
children?: ReactNode;
|
|
53
54
|
}) {
|
|
54
55
|
const parentDirection = useSnapStackDirection();
|
|
55
|
-
const
|
|
56
|
+
const buttonContentUsesHorizontal =
|
|
57
|
+
childrenShouldUseHorizontalButtonLayout(children);
|
|
58
|
+
const direction =
|
|
59
|
+
buttonContentUsesHorizontal === undefined
|
|
60
|
+
? String(props.direction ?? "vertical")
|
|
61
|
+
: buttonContentUsesHorizontal
|
|
62
|
+
? "horizontal"
|
|
63
|
+
: "vertical";
|
|
56
64
|
const isHorizontal = direction === "horizontal";
|
|
57
65
|
const justifyKey = props.justify ? String(props.justify) : undefined;
|
|
58
66
|
const justifyFlex = justifyKey ? JUSTIFY_FLEX[justifyKey] : undefined;
|
|
59
|
-
const
|
|
60
|
-
isHorizontal && horizontalChildrenAreAllButtons(children);
|
|
61
|
-
const buttonRowCount = buttonRowGrid
|
|
62
|
-
? countRenderableChildren(children)
|
|
63
|
-
: 0;
|
|
67
|
+
const allChildrenAreButtons = childrenAreAllButtons(children);
|
|
64
68
|
|
|
65
69
|
const columnsRaw = props.columns;
|
|
70
|
+
const equalWidth = props.equalWidth === true;
|
|
66
71
|
const columns =
|
|
67
72
|
typeof columnsRaw === "number" &&
|
|
68
73
|
columnsRaw >= 2 &&
|
|
@@ -71,29 +76,36 @@ export function SnapStack({
|
|
|
71
76
|
? columnsRaw
|
|
72
77
|
: undefined;
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
const equalWidthColumnCount =
|
|
80
|
+
columns ?? (equalWidth ? countRenderableChildren(children) : undefined);
|
|
81
|
+
const explicitEqualWidth =
|
|
82
|
+
isHorizontal &&
|
|
83
|
+
equalWidthColumnCount !== undefined &&
|
|
84
|
+
equalWidthColumnCount >= 1 &&
|
|
85
|
+
equalWidthColumnCount <= 6;
|
|
86
|
+
|
|
87
|
+
// Button-only stacks always default to sm; mixed horizontal stacks scale by child count.
|
|
88
|
+
// Vertical non-button stacks default to md.
|
|
89
|
+
const horizontalChildCount = isHorizontal
|
|
90
|
+
? (explicitEqualWidth
|
|
91
|
+
? equalWidthColumnCount
|
|
92
|
+
: countRenderableChildren(children))
|
|
81
93
|
: undefined;
|
|
82
94
|
const explicitGap =
|
|
83
95
|
typeof props.gap === "string" && props.gap in (isHorizontal ? HGAP : VGAP);
|
|
84
96
|
const gapKey = explicitGap
|
|
85
97
|
? String(props.gap)
|
|
86
|
-
:
|
|
87
|
-
?
|
|
98
|
+
: allChildrenAreButtons
|
|
99
|
+
? "sm"
|
|
100
|
+
: isHorizontal
|
|
101
|
+
? defaultHorizontalGapSize(horizontalChildCount)
|
|
88
102
|
: "md";
|
|
89
103
|
const gap = isHorizontal
|
|
90
104
|
? (HGAP[gapKey] ?? HGAP.md!)
|
|
91
105
|
: (VGAP[gapKey] ?? VGAP.md!);
|
|
92
|
-
const explicitColumnGrid =
|
|
93
|
-
isHorizontal && columns !== undefined && !buttonRowGrid;
|
|
94
106
|
const columnGridClass =
|
|
95
|
-
|
|
96
|
-
? COLUMN_GRID_CLASS[
|
|
107
|
+
explicitEqualWidth && equalWidthColumnCount !== undefined
|
|
108
|
+
? COLUMN_GRID_CLASS[equalWidthColumnCount]
|
|
97
109
|
: undefined;
|
|
98
110
|
|
|
99
111
|
/**
|
|
@@ -108,11 +120,18 @@ export function SnapStack({
|
|
|
108
120
|
|
|
109
121
|
const justifyBlockGrid =
|
|
110
122
|
justifyFlex &&
|
|
111
|
-
(!isHorizontal ||
|
|
123
|
+
(!isHorizontal || !explicitEqualWidth);
|
|
112
124
|
|
|
113
125
|
/** Single flex row (nowrap): peers stay side-by-side and shrink via min-w-0 / flex-1 on nested stacks. */
|
|
114
126
|
const horizontalFlexClasses =
|
|
115
127
|
"flex min-w-0 flex-row flex-nowrap items-stretch [&>*]:min-w-0";
|
|
128
|
+
const equalWidthStyle: CSSProperties | undefined =
|
|
129
|
+
explicitEqualWidth && equalWidthColumnCount !== undefined
|
|
130
|
+
? {
|
|
131
|
+
display: "grid",
|
|
132
|
+
gridTemplateColumns: `repeat(${equalWidthColumnCount}, minmax(0, 1fr))`,
|
|
133
|
+
}
|
|
134
|
+
: undefined;
|
|
116
135
|
|
|
117
136
|
return (
|
|
118
137
|
<SnapStackDirectionProvider
|
|
@@ -122,20 +141,12 @@ export function SnapStack({
|
|
|
122
141
|
className={cn(
|
|
123
142
|
rootWidthClass,
|
|
124
143
|
isHorizontal
|
|
125
|
-
?
|
|
126
|
-
buttonRowCount >= 1 &&
|
|
127
|
-
buttonRowCount <= 6 &&
|
|
128
|
-
COLUMN_GRID_CLASS[buttonRowCount]
|
|
129
|
-
? cn(
|
|
130
|
-
COLUMN_GRID_CLASS[buttonRowCount]!,
|
|
131
|
-
gap,
|
|
132
|
-
"[&>*]:w-full",
|
|
133
|
-
)
|
|
134
|
-
: explicitColumnGrid && columnGridClass
|
|
144
|
+
? explicitEqualWidth && columnGridClass
|
|
135
145
|
? cn(columnGridClass, gap)
|
|
136
146
|
: cn(horizontalFlexClasses, gap, justifyBlockGrid ? justifyFlex : undefined)
|
|
137
147
|
: cn("flex min-w-0 w-full flex-col", gap, justifyFlex),
|
|
138
148
|
)}
|
|
149
|
+
style={equalWidthStyle}
|
|
139
150
|
>
|
|
140
151
|
{children}
|
|
141
152
|
</div>
|
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { useStateStore } from "@json-render/react";
|
|
5
5
|
import { Label } from "@neynar/ui/label";
|
|
6
6
|
import { cn } from "@neynar/ui/utils";
|
|
7
|
+
import { shouldUseHorizontalButtonContent } from "../../button-orientation-utils.js";
|
|
7
8
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
8
9
|
|
|
9
10
|
export function SnapToggleGroup({
|
|
@@ -17,7 +18,6 @@ export function SnapToggleGroup({
|
|
|
17
18
|
const path = `/inputs/${name}`;
|
|
18
19
|
const label = props.label ? String(props.label) : undefined;
|
|
19
20
|
const isMultiple = Boolean(props.multiple);
|
|
20
|
-
const orientation = String(props.orientation ?? "horizontal");
|
|
21
21
|
const options = Array.isArray(props.options)
|
|
22
22
|
? (props.options as string[])
|
|
23
23
|
: [];
|
|
@@ -50,7 +50,7 @@ export function SnapToggleGroup({
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const isVertical =
|
|
53
|
+
const isVertical = !shouldUseHorizontalButtonContent(options);
|
|
54
54
|
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
55
55
|
|
|
56
56
|
return (
|
|
@@ -68,14 +68,16 @@ export function SnapToggleGroup({
|
|
|
68
68
|
const isHovered = hoveredIdx === i && !isSelected;
|
|
69
69
|
return (
|
|
70
70
|
<button
|
|
71
|
-
key={opt}
|
|
71
|
+
key={`${opt}-${i}`}
|
|
72
72
|
type="button"
|
|
73
73
|
onClick={() => toggle(opt)}
|
|
74
74
|
onPointerEnter={() => setHoveredIdx(i)}
|
|
75
75
|
onPointerLeave={() => setHoveredIdx(null)}
|
|
76
76
|
className={cn(
|
|
77
77
|
"rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
|
78
|
-
isVertical
|
|
78
|
+
isVertical
|
|
79
|
+
? "w-full"
|
|
80
|
+
: "flex-auto whitespace-nowrap",
|
|
79
81
|
)}
|
|
80
82
|
style={{
|
|
81
83
|
transition: "background-color 0.15s, color 0.15s",
|
|
@@ -63,18 +63,106 @@ const CONFETTI_COLORS = [
|
|
|
63
63
|
"#06B6D4",
|
|
64
64
|
];
|
|
65
65
|
|
|
66
|
+
const FIREWORK_COLORS = [
|
|
67
|
+
"#FFD700",
|
|
68
|
+
"#FF6B6B",
|
|
69
|
+
"#4ECDC4",
|
|
70
|
+
"#C4A7E7",
|
|
71
|
+
"#F6C177",
|
|
72
|
+
"#EBBCBA",
|
|
73
|
+
"#9CCFD8",
|
|
74
|
+
"#fff",
|
|
75
|
+
];
|
|
76
|
+
|
|
66
77
|
function ConfettiOverlay() {
|
|
67
78
|
const pieces = useMemo(
|
|
68
79
|
() =>
|
|
69
|
-
Array.from({ length: 80 }, (_, i) =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
Array.from({ length: 80 }, (_, i) => {
|
|
81
|
+
const driftX = (Math.random() - 0.5) * 120;
|
|
82
|
+
return {
|
|
83
|
+
id: i,
|
|
84
|
+
left: Math.random() * 100,
|
|
85
|
+
delay: Math.random() * 1.2,
|
|
86
|
+
duration: 2.8 + Math.random() * 1.8,
|
|
87
|
+
color:
|
|
88
|
+
CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
|
89
|
+
size: 6 + Math.random() * 8,
|
|
90
|
+
rotation: Math.random() * 360,
|
|
91
|
+
isCircle: Math.random() > 0.6,
|
|
92
|
+
driftX,
|
|
93
|
+
driftMid: -driftX * 0.4,
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
[],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
style={{
|
|
102
|
+
position: "absolute",
|
|
103
|
+
inset: 0,
|
|
104
|
+
overflow: "hidden",
|
|
105
|
+
pointerEvents: "none",
|
|
106
|
+
zIndex: 20,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{pieces.map(
|
|
110
|
+
({ id, left, delay, duration, color, size, rotation, isCircle, driftX, driftMid }) => (
|
|
111
|
+
<div
|
|
112
|
+
key={id}
|
|
113
|
+
style={
|
|
114
|
+
{
|
|
115
|
+
position: "absolute",
|
|
116
|
+
left: `${left}%`,
|
|
117
|
+
top: -20,
|
|
118
|
+
width: size,
|
|
119
|
+
height: isCircle ? size : size * 0.5,
|
|
120
|
+
backgroundColor: color,
|
|
121
|
+
borderRadius: isCircle ? "50%" : 2,
|
|
122
|
+
transform: `rotateZ(${rotation}deg)`,
|
|
123
|
+
animation: `confettiFall ${duration}s cubic-bezier(0.25,0,0.75,1) ${delay}s forwards`,
|
|
124
|
+
"--dx": `${driftX}px`,
|
|
125
|
+
"--dm": `${driftMid}px`,
|
|
126
|
+
} as CSSProperties
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
),
|
|
130
|
+
)}
|
|
131
|
+
<style>{`@keyframes confettiFall{
|
|
132
|
+
0% {top:-20px;opacity:1;transform:rotateZ(0deg) rotateY(0deg) translateX(0)}
|
|
133
|
+
20% {transform:rotateZ(144deg) rotateY(60deg) translateX(var(--dm))}
|
|
134
|
+
40% {transform:rotateZ(288deg) rotateY(120deg) translateX(0)}
|
|
135
|
+
60% {opacity:1;transform:rotateZ(432deg) rotateY(200deg) translateX(calc(-1 * var(--dm)))}
|
|
136
|
+
80% {transform:rotateZ(576deg) rotateY(280deg) translateX(var(--dx))}
|
|
137
|
+
100%{top:110%;opacity:0;transform:rotateZ(720deg) rotateY(360deg) translateX(var(--dx))}
|
|
138
|
+
}`}</style>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function FireworksOverlay() {
|
|
144
|
+
const bursts = useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
Array.from({ length: 5 }, (_, b) => ({
|
|
147
|
+
id: b,
|
|
148
|
+
x: 15 + Math.random() * 70,
|
|
149
|
+
y: 10 + Math.random() * 50,
|
|
150
|
+
delay: b * 0.5 + Math.random() * 0.2,
|
|
151
|
+
particles: Array.from({ length: 24 }, (_, p) => {
|
|
152
|
+
const angle =
|
|
153
|
+
(p / 24) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
154
|
+
const dist = 55 + Math.random() * 60;
|
|
155
|
+
return {
|
|
156
|
+
id: p,
|
|
157
|
+
vx: Math.cos(angle) * dist,
|
|
158
|
+
vy: Math.sin(angle) * dist,
|
|
159
|
+
color:
|
|
160
|
+
FIREWORK_COLORS[
|
|
161
|
+
Math.floor(Math.random() * FIREWORK_COLORS.length)
|
|
162
|
+
],
|
|
163
|
+
size: 3 + Math.random() * 3,
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
78
166
|
})),
|
|
79
167
|
[],
|
|
80
168
|
);
|
|
@@ -89,25 +177,57 @@ function ConfettiOverlay() {
|
|
|
89
177
|
zIndex: 20,
|
|
90
178
|
}}
|
|
91
179
|
>
|
|
92
|
-
{
|
|
93
|
-
<div
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
180
|
+
{bursts.map(({ id: bid, x, y, delay, particles }) => (
|
|
181
|
+
<div key={bid}>
|
|
182
|
+
<div
|
|
183
|
+
style={{
|
|
184
|
+
position: "absolute",
|
|
185
|
+
left: `${x}%`,
|
|
186
|
+
top: `${y}%`,
|
|
187
|
+
width: 12,
|
|
188
|
+
height: 12,
|
|
189
|
+
borderRadius: "50%",
|
|
190
|
+
backgroundColor: "#fff",
|
|
191
|
+
transform: "translate(-50%,-50%)",
|
|
192
|
+
animation: `fwFlash 0.4s ease-out ${delay}s both`,
|
|
193
|
+
opacity: 0,
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
{particles.map(({ id: pid, vx, vy, color, size }) => (
|
|
197
|
+
<div
|
|
198
|
+
key={pid}
|
|
199
|
+
style={
|
|
200
|
+
{
|
|
201
|
+
position: "absolute",
|
|
202
|
+
left: `${x}%`,
|
|
203
|
+
top: `${y}%`,
|
|
204
|
+
width: size,
|
|
205
|
+
height: size,
|
|
206
|
+
borderRadius: "50%",
|
|
207
|
+
backgroundColor: color,
|
|
208
|
+
transform: "translate(-50%,-50%)",
|
|
209
|
+
animation: `fwBurst 1s cubic-bezier(0.2,0,0.8,1) ${delay}s forwards`,
|
|
210
|
+
opacity: 0,
|
|
211
|
+
"--vx": `${vx}px`,
|
|
212
|
+
"--vy": `${vy}px`,
|
|
213
|
+
} as CSSProperties
|
|
214
|
+
}
|
|
215
|
+
/>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
107
218
|
))}
|
|
108
|
-
<style>{
|
|
109
|
-
|
|
110
|
-
|
|
219
|
+
<style>{`
|
|
220
|
+
@keyframes fwFlash{
|
|
221
|
+
0% {opacity:0;transform:translate(-50%,-50%) scale(0)}
|
|
222
|
+
25% {opacity:1;transform:translate(-50%,-50%) scale(2.5)}
|
|
223
|
+
100%{opacity:0;transform:translate(-50%,-50%) scale(5)}
|
|
224
|
+
}
|
|
225
|
+
@keyframes fwBurst{
|
|
226
|
+
0% {opacity:1;transform:translate(-50%,-50%) translate(0,0) scale(1)}
|
|
227
|
+
65% {opacity:1}
|
|
228
|
+
100%{opacity:0;transform:translate(-50%,-50%) translate(var(--vx),var(--vy)) scale(0)}
|
|
229
|
+
}
|
|
230
|
+
`}</style>
|
|
111
231
|
</div>
|
|
112
232
|
);
|
|
113
233
|
}
|
|
@@ -240,10 +360,13 @@ export function SnapViewCore({
|
|
|
240
360
|
}, [spec]);
|
|
241
361
|
|
|
242
362
|
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
363
|
+
const showFireworks = snap.effects?.includes("fireworks") ?? false;
|
|
243
364
|
const [confettiKey, setConfettiKey] = useState(0);
|
|
365
|
+
const [fireworksKey, setFireworksKey] = useState(0);
|
|
244
366
|
useEffect(() => {
|
|
245
367
|
if (showConfetti) setConfettiKey((k) => k + 1);
|
|
246
|
-
|
|
368
|
+
if (showFireworks) setFireworksKey((k) => k + 1);
|
|
369
|
+
}, [showConfetti, showFireworks, snap]);
|
|
247
370
|
|
|
248
371
|
const accentName = snap.theme?.accent ?? "purple";
|
|
249
372
|
|
|
@@ -326,6 +449,7 @@ export function SnapViewCore({
|
|
|
326
449
|
return (
|
|
327
450
|
<div style={{ position: "relative", width: "100%" }}>
|
|
328
451
|
{showConfetti && <ConfettiOverlay key={confettiKey} />}
|
|
452
|
+
{showFireworks && <FireworksOverlay key={fireworksKey} />}
|
|
329
453
|
{loadingOverlay === undefined ? (
|
|
330
454
|
<SnapLoadingOverlay
|
|
331
455
|
appearance={appearance}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
const SnapItemGroupBorderContext = createContext(false);
|
|
4
|
+
|
|
5
|
+
export const SnapItemGroupBorderProvider =
|
|
6
|
+
SnapItemGroupBorderContext.Provider;
|
|
7
|
+
|
|
8
|
+
export function useSnapItemGroupHasBorder() {
|
|
9
|
+
return useContext(SnapItemGroupBorderContext);
|
|
10
|
+
}
|
|
@@ -5,6 +5,7 @@ import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
|
5
5
|
import { ExternalLink } from "lucide-react-native";
|
|
6
6
|
import { useSnapPalette } from "../use-snap-palette";
|
|
7
7
|
import { useSnapTheme } from "../theme";
|
|
8
|
+
import { useSnapStackDirection } from "../stack-direction-context";
|
|
8
9
|
import { ICON_MAP } from "./snap-icon";
|
|
9
10
|
|
|
10
11
|
function isExternalLinkAction(
|
|
@@ -32,12 +33,13 @@ export function SnapActionButton({
|
|
|
32
33
|
|
|
33
34
|
const textColor = isPrimary ? "#fff" : colors.text;
|
|
34
35
|
const iconColor = isPrimary ? "#fff" : colors.text;
|
|
36
|
+
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
35
37
|
|
|
36
38
|
const on = (element as unknown as { on?: Record<string, unknown> }).on;
|
|
37
39
|
const showExternalIcon = isExternalLinkAction(on);
|
|
38
40
|
|
|
39
41
|
return (
|
|
40
|
-
<View style={styles.outer}>
|
|
42
|
+
<View style={inHorizontalStack ? styles.outerHorizontal : styles.outer}>
|
|
41
43
|
<Pressable
|
|
42
44
|
style={({ pressed }) => [
|
|
43
45
|
styles.btn,
|
|
@@ -77,7 +79,18 @@ export function SnapActionButton({
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const styles = StyleSheet.create({
|
|
80
|
-
outer: {
|
|
82
|
+
outer: {
|
|
83
|
+
width: "100%",
|
|
84
|
+
minWidth: 0,
|
|
85
|
+
alignSelf: "stretch",
|
|
86
|
+
},
|
|
87
|
+
outerHorizontal: {
|
|
88
|
+
flexGrow: 1,
|
|
89
|
+
flexShrink: 1,
|
|
90
|
+
flexBasis: "auto",
|
|
91
|
+
minWidth: 0,
|
|
92
|
+
alignSelf: "stretch",
|
|
93
|
+
},
|
|
81
94
|
btn: {
|
|
82
95
|
paddingHorizontal: 16,
|
|
83
96
|
borderRadius: 10,
|
|
@@ -18,6 +18,7 @@ export function SnapCellGrid({
|
|
|
18
18
|
const rows = Number(props.rows ?? 2);
|
|
19
19
|
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
20
20
|
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
21
|
+
const squareCells = props.cellAspectRatio === "square";
|
|
21
22
|
const gap = String(props.gap ?? "sm");
|
|
22
23
|
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
23
24
|
const gapPx = gapMap[gap] ?? 1;
|
|
@@ -101,18 +102,35 @@ export function SnapCellGrid({
|
|
|
101
102
|
|
|
102
103
|
// Two-tone ring: outer View with contrasting border, inner View with inverse border
|
|
103
104
|
const cellView = selected ? (
|
|
104
|
-
<View
|
|
105
|
+
<View
|
|
106
|
+
style={[
|
|
107
|
+
styles.cell,
|
|
108
|
+
squareCells ? styles.squareCell : { height: rowHeight },
|
|
109
|
+
{ borderWidth: 1, borderColor: ringOuter, borderRadius: 4 },
|
|
110
|
+
]}
|
|
111
|
+
>
|
|
105
112
|
<View
|
|
106
113
|
style={[
|
|
107
114
|
styles.innerCell,
|
|
108
|
-
{
|
|
115
|
+
{
|
|
116
|
+
backgroundColor: bg,
|
|
117
|
+
borderWidth: 1,
|
|
118
|
+
borderColor: ringInner,
|
|
119
|
+
borderRadius: 3,
|
|
120
|
+
},
|
|
109
121
|
]}
|
|
110
122
|
>
|
|
111
123
|
{cellContent}
|
|
112
124
|
</View>
|
|
113
125
|
</View>
|
|
114
126
|
) : (
|
|
115
|
-
<View
|
|
127
|
+
<View
|
|
128
|
+
style={[
|
|
129
|
+
styles.cell,
|
|
130
|
+
squareCells ? styles.squareCell : { height: rowHeight },
|
|
131
|
+
{ backgroundColor: bg },
|
|
132
|
+
]}
|
|
133
|
+
>
|
|
116
134
|
{cellContent}
|
|
117
135
|
</View>
|
|
118
136
|
);
|
|
@@ -141,7 +159,17 @@ export function SnapCellGrid({
|
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
return (
|
|
144
|
-
<View
|
|
162
|
+
<View
|
|
163
|
+
style={[
|
|
164
|
+
styles.wrap,
|
|
165
|
+
{
|
|
166
|
+
gap: gapPx,
|
|
167
|
+
backgroundColor: colors.muted,
|
|
168
|
+
padding: 4,
|
|
169
|
+
borderRadius: 8,
|
|
170
|
+
},
|
|
171
|
+
]}
|
|
172
|
+
>
|
|
145
173
|
{rowEls}
|
|
146
174
|
</View>
|
|
147
175
|
);
|
|
@@ -156,6 +184,10 @@ const styles = StyleSheet.create({
|
|
|
156
184
|
alignItems: "center",
|
|
157
185
|
justifyContent: "center",
|
|
158
186
|
},
|
|
187
|
+
squareCell: {
|
|
188
|
+
aspectRatio: 1,
|
|
189
|
+
width: "100%",
|
|
190
|
+
},
|
|
159
191
|
innerCell: {
|
|
160
192
|
width: "100%",
|
|
161
193
|
height: "100%",
|