@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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const MAX_HORIZONTAL_TOTAL_LENGTH = {
|
|
2
|
+
2: 20,
|
|
3
|
+
3: 15,
|
|
4
|
+
4: 11,
|
|
5
|
+
5: 8,
|
|
6
|
+
};
|
|
7
|
+
function displayLength(label) {
|
|
8
|
+
return Array.from(label.trim().replace(/\s+/g, " ")).length;
|
|
9
|
+
}
|
|
10
|
+
export function getButtonContentOrientation(labels) {
|
|
11
|
+
const lengths = labels
|
|
12
|
+
.map((label) => displayLength(label))
|
|
13
|
+
.filter((length) => length > 0);
|
|
14
|
+
const count = lengths.length;
|
|
15
|
+
if (count <= 1)
|
|
16
|
+
return "horizontal";
|
|
17
|
+
const maxTotalLength = MAX_HORIZONTAL_TOTAL_LENGTH[count] ?? 0;
|
|
18
|
+
if (maxTotalLength === 0)
|
|
19
|
+
return "vertical";
|
|
20
|
+
const totalLength = lengths.reduce((sum, length) => sum + length, 0);
|
|
21
|
+
return totalLength <= maxTotalLength ? "horizontal" : "vertical";
|
|
22
|
+
}
|
|
23
|
+
export function shouldUseHorizontalButtonContent(labels) {
|
|
24
|
+
return getButtonContentOrientation(labels) === "horizontal";
|
|
25
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -5,13 +5,14 @@ export declare const SUPPORTED_SPEC_VERSIONS: readonly ["1.0", "2.0"];
|
|
|
5
5
|
export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
|
|
6
6
|
export declare const SNAP_PAYLOAD_HEADER: "X-Snap-Payload";
|
|
7
7
|
export declare const MEDIA_TYPE: "application/vnd.farcaster.snap+json";
|
|
8
|
-
export declare const EFFECT_VALUES: readonly ["confetti"];
|
|
8
|
+
export declare const EFFECT_VALUES: readonly ["confetti", "fireworks"];
|
|
9
9
|
export declare const POST_GRID_TAP_KEY: "grid_tap";
|
|
10
10
|
export declare const GRID_MIN_COLS = 2;
|
|
11
11
|
export declare const GRID_MAX_COLS = 32;
|
|
12
12
|
export declare const GRID_MIN_ROWS = 2;
|
|
13
13
|
export declare const GRID_MAX_ROWS = 16;
|
|
14
14
|
export declare const GRID_GAP_VALUES: readonly ["none", "sm", "md", "lg"];
|
|
15
|
+
export declare const GRID_CELL_ASPECT_RATIO_VALUES: readonly ["auto", "square"];
|
|
15
16
|
export declare const MAX_ELEMENTS = 64;
|
|
16
17
|
export declare const MAX_ROOT_CHILDREN = 7;
|
|
17
18
|
export declare const MAX_CHILDREN = 6;
|
package/dist/constants.js
CHANGED
|
@@ -7,7 +7,7 @@ export const SUPPORTED_SPEC_VERSIONS = [
|
|
|
7
7
|
];
|
|
8
8
|
export const SNAP_PAYLOAD_HEADER = "X-Snap-Payload";
|
|
9
9
|
export const MEDIA_TYPE = "application/vnd.farcaster.snap+json";
|
|
10
|
-
export const EFFECT_VALUES = ["confetti"];
|
|
10
|
+
export const EFFECT_VALUES = ["confetti", "fireworks"];
|
|
11
11
|
// ─── Pixel grid ────────────────────────────────────────
|
|
12
12
|
export const POST_GRID_TAP_KEY = "grid_tap";
|
|
13
13
|
export const GRID_MIN_COLS = 2;
|
|
@@ -15,6 +15,7 @@ export const GRID_MAX_COLS = 32;
|
|
|
15
15
|
export const GRID_MIN_ROWS = 2;
|
|
16
16
|
export const GRID_MAX_ROWS = 16;
|
|
17
17
|
export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"];
|
|
18
|
+
export const GRID_CELL_ASPECT_RATIO_VALUES = ["auto", "square"];
|
|
18
19
|
// ─── Snap structural limits ───────────────────────────
|
|
19
20
|
export const MAX_ELEMENTS = 64;
|
|
20
21
|
export const MAX_ROOT_CHILDREN = 7;
|
|
@@ -44,11 +44,11 @@ export function SnapActionButton({ element, emit, }) {
|
|
|
44
44
|
};
|
|
45
45
|
return (
|
|
46
46
|
/**
|
|
47
|
-
* In a horizontal stack, `flex-
|
|
48
|
-
* In a vertical stack,
|
|
49
|
-
*
|
|
47
|
+
* In a horizontal stack, `flex-auto` lets the row fill available width while
|
|
48
|
+
* preserving content-proportional button widths. In a vertical stack, flex
|
|
49
|
+
* growth would silently stretch button height; stick to `w-full`.
|
|
50
50
|
*/
|
|
51
51
|
_jsx("div", { className: inHorizontalStack
|
|
52
|
-
? "
|
|
53
|
-
: "w-full min-w-0", children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("w-full gap-2"), style: style, onClick: () => emit("press"), onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label, showExternalIcon && (_jsx(ExternalLink, { size: 14, style: { opacity: 0.6 } }))] }) }));
|
|
52
|
+
? "min-w-0 flex-auto"
|
|
53
|
+
: "w-full min-w-0", style: inHorizontalStack ? { flex: "1 1 auto" } : undefined, children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("w-full gap-2"), style: style, onClick: () => emit("press"), onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label, showExternalIcon && (_jsx(ExternalLink, { size: 14, style: { opacity: 0.6 } }))] }) }));
|
|
54
54
|
}
|
|
@@ -19,6 +19,7 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
19
19
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
20
20
|
const gapPx = gapMap[gap] ?? 1;
|
|
21
21
|
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
22
|
+
const squareCells = props.cellAspectRatio === "square";
|
|
22
23
|
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
23
24
|
const tapPath = `/inputs/${name}`;
|
|
24
25
|
const tapRaw = get(tapPath);
|
|
@@ -63,7 +64,9 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
63
64
|
emit("press");
|
|
64
65
|
};
|
|
65
66
|
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
66
|
-
const emptyCellBg = colors.mode === "dark"
|
|
67
|
+
const emptyCellBg = colors.mode === "dark"
|
|
68
|
+
? "rgba(255, 255, 255, 0.05)"
|
|
69
|
+
: "rgba(0, 0, 0, 0.05)";
|
|
67
70
|
const cellEls = [];
|
|
68
71
|
for (let r = 0; r < rows; r++) {
|
|
69
72
|
for (let c = 0; c < cols; c++) {
|
|
@@ -80,7 +83,8 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
: undefined, className: cn("flex items-center justify-center rounded text-xs font-semibold", interactive ? "cursor-pointer select-none" : "cursor-default"), style: {
|
|
83
|
-
height: rowHeight,
|
|
86
|
+
height: squareCells ? undefined : rowHeight,
|
|
87
|
+
aspectRatio: squareCells ? "1 / 1" : undefined,
|
|
84
88
|
background: bg,
|
|
85
89
|
color: textColor,
|
|
86
90
|
boxShadow: selected
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Children, Fragment } from "react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
6
|
+
import { SnapItemGroupBorderProvider } from "./item-layout-context.js";
|
|
6
7
|
const GAP_MAP = {
|
|
7
8
|
none: "gap-0",
|
|
8
9
|
sm: "gap-1",
|
|
@@ -15,5 +16,5 @@ export function SnapItemGroup({ element: { props }, children, }) {
|
|
|
15
16
|
const gap = GAP_MAP[String(props.gap ?? "sm")] ?? "gap-1";
|
|
16
17
|
const items = Children.toArray(children);
|
|
17
18
|
const colors = useSnapColors();
|
|
18
|
-
return (_jsx("div", { className: cn("flex flex-col", border && "rounded-lg border", gap), style: border ? { borderColor: colors.border } : undefined, children: items.map((child, i) => (_jsxs(Fragment, { children: [separator && i > 0 && (_jsx("div", { className: "h-px", style: { backgroundColor: colors.border } })), child] }, i))) }));
|
|
19
|
+
return (_jsx(SnapItemGroupBorderProvider, { value: border, children: _jsx("div", { className: cn("flex flex-col", border && "rounded-lg border", gap), style: border ? { borderColor: colors.border } : undefined, children: items.map((child, i) => (_jsxs(Fragment, { children: [separator && i > 0 && (_jsx("div", { className: "h-px", style: { backgroundColor: colors.border } })), child] }, i))) }) }));
|
|
19
20
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const SnapItemGroupBorderContext = createContext(false);
|
|
4
|
+
export const SnapItemGroupBorderProvider = SnapItemGroupBorderContext.Provider;
|
|
5
|
+
export function useSnapItemGroupHasBorder() {
|
|
6
|
+
return useContext(SnapItemGroupBorderContext);
|
|
7
|
+
}
|
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Item, ItemContent, ItemTitle, ItemDescription, ItemActions, } from "@neynar/ui/item";
|
|
3
|
+
import { Item, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemMedia, } from "@neynar/ui/item";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
6
6
|
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
7
|
+
import { ICON_MAP } from "./icon.js";
|
|
8
|
+
import { useSnapItemGroupHasBorder } from "./item-layout-context.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
|
+
}
|
|
7
30
|
export function SnapItem({ element: { props, children: childIds }, children, }) {
|
|
8
31
|
const title = String(props.title ?? "");
|
|
9
32
|
const description = props.description ? String(props.description) : undefined;
|
|
33
|
+
const media = parseItemMedia(props.media);
|
|
10
34
|
const colors = useSnapColors();
|
|
35
|
+
const inBorderedGroup = useSnapItemGroupHasBorder();
|
|
11
36
|
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
12
|
-
|
|
37
|
+
const MediaIcon = media?.variant === "icon" ? ICON_MAP[media.name] : undefined;
|
|
38
|
+
return (_jsxs(Item, { className: cn("gap-2 py-1.5", inBorderedGroup ? "px-2" : "px-0",
|
|
13
39
|
/** Horizontal: share width with peers. Vertical: don't fill column height. */
|
|
14
|
-
inHorizontalStack && "flex-1"),
|
|
40
|
+
inHorizontalStack && "flex-1"), style: {
|
|
41
|
+
columnGap: 8,
|
|
42
|
+
paddingInline: inBorderedGroup ? 8 : 0,
|
|
43
|
+
}, children: [media?.variant === "icon" && MediaIcon && (_jsx(ItemMedia, { variant: "icon", className: "self-center translate-y-0", style: { alignSelf: "center", transform: "none" }, children: _jsx(MediaIcon, { size: 20, style: { color: colors.colorHex(media.color) } }) })), media?.variant === "image" && (_jsx(ItemMedia, { variant: "image", className: "self-center translate-y-0", style: {
|
|
44
|
+
alignSelf: "center",
|
|
45
|
+
borderRadius: media.round ? "9999px" : undefined,
|
|
46
|
+
transform: "none",
|
|
47
|
+
}, children: _jsx("img", { src: media.url, alt: media.alt ?? "", className: "size-full object-cover" }) })), _jsxs(ItemContent, { className: "gap-0", children: [_jsx(ItemTitle, { style: { color: colors.text }, children: title }), description && (_jsx(ItemDescription, { className: "mt-0 text-xs leading-snug", style: {
|
|
48
|
+
color: colors.textMuted,
|
|
49
|
+
fontSize: 12,
|
|
50
|
+
lineHeight: "16px",
|
|
51
|
+
}, children: description }))] }), childIds && childIds.length > 0 && (_jsx(ItemActions, { className: "gap-1.5 self-center", style: { alignSelf: "center", columnGap: 6 }, children: children }))] }));
|
|
15
52
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "@neynar/ui/utils";
|
|
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: "gap-0",
|
|
8
|
-
sm: "gap-
|
|
8
|
+
sm: "gap-1",
|
|
9
9
|
md: "gap-4",
|
|
10
10
|
lg: "gap-6",
|
|
11
11
|
};
|
|
@@ -22,52 +22,60 @@ const JUSTIFY_FLEX = {
|
|
|
22
22
|
between: "justify-between",
|
|
23
23
|
around: "justify-around",
|
|
24
24
|
};
|
|
25
|
-
/** Equal
|
|
25
|
+
/** Equal-width cell count for explicit `equalWidth` / `columns` props. */
|
|
26
26
|
const COLUMN_GRID_CLASS = {
|
|
27
|
-
1: "
|
|
28
|
-
2: "
|
|
29
|
-
3: "
|
|
30
|
-
4: "
|
|
31
|
-
5: "
|
|
32
|
-
6: "
|
|
27
|
+
1: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
28
|
+
2: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
29
|
+
3: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
30
|
+
4: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
31
|
+
5: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
32
|
+
6: "auto-rows-auto items-stretch [&>*]:min-w-0 [&>*]:w-full",
|
|
33
33
|
};
|
|
34
34
|
export function SnapStack({ element: { props }, children, }) {
|
|
35
35
|
const parentDirection = useSnapStackDirection();
|
|
36
|
-
const
|
|
36
|
+
const buttonContentUsesHorizontal = childrenShouldUseHorizontalButtonLayout(children);
|
|
37
|
+
const direction = buttonContentUsesHorizontal === undefined
|
|
38
|
+
? String(props.direction ?? "vertical")
|
|
39
|
+
: buttonContentUsesHorizontal
|
|
40
|
+
? "horizontal"
|
|
41
|
+
: "vertical";
|
|
37
42
|
const isHorizontal = direction === "horizontal";
|
|
38
43
|
const justifyKey = props.justify ? String(props.justify) : undefined;
|
|
39
44
|
const justifyFlex = justifyKey ? JUSTIFY_FLEX[justifyKey] : undefined;
|
|
40
|
-
const
|
|
41
|
-
const buttonRowCount = buttonRowGrid
|
|
42
|
-
? countRenderableChildren(children)
|
|
43
|
-
: 0;
|
|
45
|
+
const allChildrenAreButtons = childrenAreAllButtons(children);
|
|
44
46
|
const columnsRaw = props.columns;
|
|
47
|
+
const equalWidth = props.equalWidth === true;
|
|
45
48
|
const columns = typeof columnsRaw === "number" &&
|
|
46
49
|
columnsRaw >= 2 &&
|
|
47
50
|
columnsRaw <= 6 &&
|
|
48
51
|
Number.isInteger(columnsRaw)
|
|
49
52
|
? columnsRaw
|
|
50
53
|
: undefined;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const equalWidthColumnCount = columns ?? (equalWidth ? countRenderableChildren(children) : undefined);
|
|
55
|
+
const explicitEqualWidth = isHorizontal &&
|
|
56
|
+
equalWidthColumnCount !== undefined &&
|
|
57
|
+
equalWidthColumnCount >= 1 &&
|
|
58
|
+
equalWidthColumnCount <= 6;
|
|
59
|
+
// Button-only stacks always default to sm; mixed horizontal stacks scale by child count.
|
|
60
|
+
// Vertical non-button stacks default to md.
|
|
61
|
+
const horizontalChildCount = isHorizontal
|
|
62
|
+
? (explicitEqualWidth
|
|
63
|
+
? equalWidthColumnCount
|
|
64
|
+
: countRenderableChildren(children))
|
|
58
65
|
: undefined;
|
|
59
66
|
const explicitGap = typeof props.gap === "string" && props.gap in (isHorizontal ? HGAP : VGAP);
|
|
60
67
|
const gapKey = explicitGap
|
|
61
68
|
? String(props.gap)
|
|
62
|
-
:
|
|
63
|
-
?
|
|
64
|
-
:
|
|
69
|
+
: allChildrenAreButtons
|
|
70
|
+
? "sm"
|
|
71
|
+
: isHorizontal
|
|
72
|
+
? defaultHorizontalGapSize(horizontalChildCount)
|
|
73
|
+
: "md";
|
|
65
74
|
const gap = isHorizontal
|
|
66
75
|
? (HGAP[gapKey] ?? HGAP.md)
|
|
67
76
|
: (VGAP[gapKey] ?? VGAP.md);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
? COLUMN_GRID_CLASS[columns]
|
|
77
|
+
const columnGridClass = explicitEqualWidth && equalWidthColumnCount !== undefined
|
|
78
|
+
? COLUMN_GRID_CLASS[equalWidthColumnCount]
|
|
71
79
|
: undefined;
|
|
72
80
|
/**
|
|
73
81
|
* Row peers under a horizontal stack must shrink and share width (`flex-1` + `min-w-0`).
|
|
@@ -79,17 +87,18 @@ export function SnapStack({ element: { props }, children, }) {
|
|
|
79
87
|
? "min-w-0 flex-1 basis-0 max-w-full"
|
|
80
88
|
: "w-full min-w-0";
|
|
81
89
|
const justifyBlockGrid = justifyFlex &&
|
|
82
|
-
(!isHorizontal ||
|
|
90
|
+
(!isHorizontal || !explicitEqualWidth);
|
|
83
91
|
/** Single flex row (nowrap): peers stay side-by-side and shrink via min-w-0 / flex-1 on nested stacks. */
|
|
84
92
|
const horizontalFlexClasses = "flex min-w-0 flex-row flex-nowrap items-stretch [&>*]:min-w-0";
|
|
93
|
+
const equalWidthStyle = explicitEqualWidth && equalWidthColumnCount !== undefined
|
|
94
|
+
? {
|
|
95
|
+
display: "grid",
|
|
96
|
+
gridTemplateColumns: `repeat(${equalWidthColumnCount}, minmax(0, 1fr))`,
|
|
97
|
+
}
|
|
98
|
+
: undefined;
|
|
85
99
|
return (_jsx(SnapStackDirectionProvider, { direction: isHorizontal ? "horizontal" : "vertical", children: _jsx("div", { className: cn(rootWidthClass, isHorizontal
|
|
86
|
-
?
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
? cn(COLUMN_GRID_CLASS[buttonRowCount], gap, "[&>*]:w-full")
|
|
91
|
-
: explicitColumnGrid && columnGridClass
|
|
92
|
-
? cn(columnGridClass, gap)
|
|
93
|
-
: cn(horizontalFlexClasses, gap, justifyBlockGrid ? justifyFlex : undefined)
|
|
94
|
-
: cn("flex min-w-0 w-full flex-col", gap, justifyFlex)), children: children }) }));
|
|
100
|
+
? explicitEqualWidth && columnGridClass
|
|
101
|
+
? cn(columnGridClass, gap)
|
|
102
|
+
: cn(horizontalFlexClasses, gap, justifyBlockGrid ? justifyFlex : undefined)
|
|
103
|
+
: cn("flex min-w-0 w-full flex-col", gap, justifyFlex)), style: equalWidthStyle, children: children }) }));
|
|
95
104
|
}
|
|
@@ -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.js";
|
|
8
9
|
export function SnapToggleGroup({ element: { props }, }) {
|
|
9
10
|
const { get, set } = useStateStore();
|
|
@@ -12,7 +13,6 @@ export function SnapToggleGroup({ element: { props }, }) {
|
|
|
12
13
|
const path = `/inputs/${name}`;
|
|
13
14
|
const label = props.label ? String(props.label) : undefined;
|
|
14
15
|
const isMultiple = Boolean(props.multiple);
|
|
15
|
-
const orientation = String(props.orientation ?? "horizontal");
|
|
16
16
|
const options = Array.isArray(props.options)
|
|
17
17
|
? props.options
|
|
18
18
|
: [];
|
|
@@ -43,12 +43,14 @@ export function SnapToggleGroup({ element: { props }, }) {
|
|
|
43
43
|
set(path, opt);
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
const isVertical =
|
|
46
|
+
const isVertical = !shouldUseHorizontalButtonContent(options);
|
|
47
47
|
const [hoveredIdx, setHoveredIdx] = useState(null);
|
|
48
48
|
return (_jsxs("div", { className: "w-full space-y-1.5", children: [label && _jsx(Label, { style: { color: colors.text }, children: label }), _jsx("div", { className: cn("flex gap-1 rounded-lg p-1", isVertical ? "flex-col" : "flex-row"), style: { backgroundColor: colors.muted }, children: options.map((opt, i) => {
|
|
49
49
|
const isSelected = selected.includes(opt);
|
|
50
50
|
const isHovered = hoveredIdx === i && !isSelected;
|
|
51
|
-
return (_jsx("button", { type: "button", onClick: () => toggle(opt), onPointerEnter: () => setHoveredIdx(i), onPointerLeave: () => setHoveredIdx(null), className: cn("rounded-md px-3 py-2 text-sm font-medium transition-colors", isVertical
|
|
51
|
+
return (_jsx("button", { type: "button", onClick: () => toggle(opt), onPointerEnter: () => setHoveredIdx(i), onPointerLeave: () => setHoveredIdx(null), className: cn("rounded-md px-3 py-2 text-sm font-medium transition-colors", isVertical
|
|
52
|
+
? "w-full"
|
|
53
|
+
: "flex-auto whitespace-nowrap"), style: {
|
|
52
54
|
transition: "background-color 0.15s, color 0.15s",
|
|
53
55
|
...(isSelected
|
|
54
56
|
? {
|
|
@@ -61,6 +63,6 @@ export function SnapToggleGroup({ element: { props }, }) {
|
|
|
61
63
|
? (colors.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)")
|
|
62
64
|
: (colors.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)"),
|
|
63
65
|
}),
|
|
64
|
-
}, children: opt }, opt));
|
|
66
|
+
}, children: opt }, `${opt}-${i}`));
|
|
65
67
|
}) })] }));
|
|
66
68
|
}
|
|
@@ -47,33 +47,119 @@ const CONFETTI_COLORS = [
|
|
|
47
47
|
"#EF4444",
|
|
48
48
|
"#06B6D4",
|
|
49
49
|
];
|
|
50
|
+
const FIREWORK_COLORS = [
|
|
51
|
+
"#FFD700",
|
|
52
|
+
"#FF6B6B",
|
|
53
|
+
"#4ECDC4",
|
|
54
|
+
"#C4A7E7",
|
|
55
|
+
"#F6C177",
|
|
56
|
+
"#EBBCBA",
|
|
57
|
+
"#9CCFD8",
|
|
58
|
+
"#fff",
|
|
59
|
+
];
|
|
50
60
|
function ConfettiOverlay() {
|
|
51
|
-
const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) => {
|
|
62
|
+
const driftX = (Math.random() - 0.5) * 120;
|
|
63
|
+
return {
|
|
64
|
+
id: i,
|
|
65
|
+
left: Math.random() * 100,
|
|
66
|
+
delay: Math.random() * 1.2,
|
|
67
|
+
duration: 2.8 + Math.random() * 1.8,
|
|
68
|
+
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
|
69
|
+
size: 6 + Math.random() * 8,
|
|
70
|
+
rotation: Math.random() * 360,
|
|
71
|
+
isCircle: Math.random() > 0.6,
|
|
72
|
+
driftX,
|
|
73
|
+
driftMid: -driftX * 0.4,
|
|
74
|
+
};
|
|
75
|
+
}), []);
|
|
60
76
|
return (_jsxs("div", { style: {
|
|
61
77
|
position: "absolute",
|
|
62
78
|
inset: 0,
|
|
63
79
|
overflow: "hidden",
|
|
64
80
|
pointerEvents: "none",
|
|
65
81
|
zIndex: 20,
|
|
66
|
-
}, children: [pieces.map(({ id, left, delay, duration, color, size, rotation }) => (_jsx("div", { style: {
|
|
82
|
+
}, children: [pieces.map(({ id, left, delay, duration, color, size, rotation, isCircle, driftX, driftMid }) => (_jsx("div", { style: {
|
|
67
83
|
position: "absolute",
|
|
68
84
|
left: `${left}%`,
|
|
69
85
|
top: -20,
|
|
70
86
|
width: size,
|
|
71
|
-
height: size * 0.
|
|
87
|
+
height: isCircle ? size : size * 0.5,
|
|
72
88
|
backgroundColor: color,
|
|
73
|
-
borderRadius: 2,
|
|
74
|
-
transform: `
|
|
75
|
-
animation: `confettiFall ${duration}s
|
|
76
|
-
|
|
89
|
+
borderRadius: isCircle ? "50%" : 2,
|
|
90
|
+
transform: `rotateZ(${rotation}deg)`,
|
|
91
|
+
animation: `confettiFall ${duration}s cubic-bezier(0.25,0,0.75,1) ${delay}s forwards`,
|
|
92
|
+
"--dx": `${driftX}px`,
|
|
93
|
+
"--dm": `${driftMid}px`,
|
|
94
|
+
} }, id))), _jsx("style", { children: `@keyframes confettiFall{
|
|
95
|
+
0% {top:-20px;opacity:1;transform:rotateZ(0deg) rotateY(0deg) translateX(0)}
|
|
96
|
+
20% {transform:rotateZ(144deg) rotateY(60deg) translateX(var(--dm))}
|
|
97
|
+
40% {transform:rotateZ(288deg) rotateY(120deg) translateX(0)}
|
|
98
|
+
60% {opacity:1;transform:rotateZ(432deg) rotateY(200deg) translateX(calc(-1 * var(--dm)))}
|
|
99
|
+
80% {transform:rotateZ(576deg) rotateY(280deg) translateX(var(--dx))}
|
|
100
|
+
100%{top:110%;opacity:0;transform:rotateZ(720deg) rotateY(360deg) translateX(var(--dx))}
|
|
101
|
+
}` })] }));
|
|
102
|
+
}
|
|
103
|
+
function FireworksOverlay() {
|
|
104
|
+
const bursts = useMemo(() => Array.from({ length: 5 }, (_, b) => ({
|
|
105
|
+
id: b,
|
|
106
|
+
x: 15 + Math.random() * 70,
|
|
107
|
+
y: 10 + Math.random() * 50,
|
|
108
|
+
delay: b * 0.5 + Math.random() * 0.2,
|
|
109
|
+
particles: Array.from({ length: 24 }, (_, p) => {
|
|
110
|
+
const angle = (p / 24) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
111
|
+
const dist = 55 + Math.random() * 60;
|
|
112
|
+
return {
|
|
113
|
+
id: p,
|
|
114
|
+
vx: Math.cos(angle) * dist,
|
|
115
|
+
vy: Math.sin(angle) * dist,
|
|
116
|
+
color: FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)],
|
|
117
|
+
size: 3 + Math.random() * 3,
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
})), []);
|
|
121
|
+
return (_jsxs("div", { style: {
|
|
122
|
+
position: "absolute",
|
|
123
|
+
inset: 0,
|
|
124
|
+
overflow: "hidden",
|
|
125
|
+
pointerEvents: "none",
|
|
126
|
+
zIndex: 20,
|
|
127
|
+
}, children: [bursts.map(({ id: bid, x, y, delay, particles }) => (_jsxs("div", { children: [_jsx("div", { style: {
|
|
128
|
+
position: "absolute",
|
|
129
|
+
left: `${x}%`,
|
|
130
|
+
top: `${y}%`,
|
|
131
|
+
width: 12,
|
|
132
|
+
height: 12,
|
|
133
|
+
borderRadius: "50%",
|
|
134
|
+
backgroundColor: "#fff",
|
|
135
|
+
transform: "translate(-50%,-50%)",
|
|
136
|
+
animation: `fwFlash 0.4s ease-out ${delay}s both`,
|
|
137
|
+
opacity: 0,
|
|
138
|
+
} }), particles.map(({ id: pid, vx, vy, color, size }) => (_jsx("div", { style: {
|
|
139
|
+
position: "absolute",
|
|
140
|
+
left: `${x}%`,
|
|
141
|
+
top: `${y}%`,
|
|
142
|
+
width: size,
|
|
143
|
+
height: size,
|
|
144
|
+
borderRadius: "50%",
|
|
145
|
+
backgroundColor: color,
|
|
146
|
+
transform: "translate(-50%,-50%)",
|
|
147
|
+
animation: `fwBurst 1s cubic-bezier(0.2,0,0.8,1) ${delay}s forwards`,
|
|
148
|
+
opacity: 0,
|
|
149
|
+
"--vx": `${vx}px`,
|
|
150
|
+
"--vy": `${vy}px`,
|
|
151
|
+
} }, pid)))] }, bid))), _jsx("style", { children: `
|
|
152
|
+
@keyframes fwFlash{
|
|
153
|
+
0% {opacity:0;transform:translate(-50%,-50%) scale(0)}
|
|
154
|
+
25% {opacity:1;transform:translate(-50%,-50%) scale(2.5)}
|
|
155
|
+
100%{opacity:0;transform:translate(-50%,-50%) scale(5)}
|
|
156
|
+
}
|
|
157
|
+
@keyframes fwBurst{
|
|
158
|
+
0% {opacity:1;transform:translate(-50%,-50%) translate(0,0) scale(1)}
|
|
159
|
+
65% {opacity:1}
|
|
160
|
+
100%{opacity:0;transform:translate(-50%,-50%) translate(var(--vx),var(--vy)) scale(0)}
|
|
161
|
+
}
|
|
162
|
+
` })] }));
|
|
77
163
|
}
|
|
78
164
|
export function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
79
165
|
const isDark = appearance === "dark";
|
|
@@ -156,11 +242,15 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
156
242
|
setPageKey((k) => k + 1);
|
|
157
243
|
}, [spec]);
|
|
158
244
|
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
245
|
+
const showFireworks = snap.effects?.includes("fireworks") ?? false;
|
|
159
246
|
const [confettiKey, setConfettiKey] = useState(0);
|
|
247
|
+
const [fireworksKey, setFireworksKey] = useState(0);
|
|
160
248
|
useEffect(() => {
|
|
161
249
|
if (showConfetti)
|
|
162
250
|
setConfettiKey((k) => k + 1);
|
|
163
|
-
|
|
251
|
+
if (showFireworks)
|
|
252
|
+
setFireworksKey((k) => k + 1);
|
|
253
|
+
}, [showConfetti, showFireworks, snap]);
|
|
164
254
|
const accentName = snap.theme?.accent ?? "purple";
|
|
165
255
|
const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
|
|
166
256
|
const previewSurfaceStyle = useMemo(() => {
|
|
@@ -226,7 +316,7 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
226
316
|
break;
|
|
227
317
|
}
|
|
228
318
|
}, [handlers]);
|
|
229
|
-
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
319
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), showFireworks && _jsx(FireworksOverlay, {}, fireworksKey), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
230
320
|
applyStatePaths(stateRef.current, changes);
|
|
231
321
|
}, onAction: handleAction }, pageKey) }) })] }));
|
|
232
322
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
const SnapItemGroupBorderContext = createContext(false);
|
|
3
|
+
export const SnapItemGroupBorderProvider = SnapItemGroupBorderContext.Provider;
|
|
4
|
+
export function useSnapItemGroupHasBorder() {
|
|
5
|
+
return useContext(SnapItemGroupBorderContext);
|
|
6
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
|
3
3
|
import { ExternalLink } from "lucide-react-native";
|
|
4
4
|
import { useSnapPalette } from "../use-snap-palette.js";
|
|
5
5
|
import { useSnapTheme } from "../theme.js";
|
|
6
|
+
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
6
7
|
import { ICON_MAP } from "./snap-icon.js";
|
|
7
8
|
function isExternalLinkAction(on) {
|
|
8
9
|
if (!on)
|
|
@@ -22,9 +23,10 @@ export function SnapActionButton({ element, emit, }) {
|
|
|
22
23
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
23
24
|
const textColor = isPrimary ? "#fff" : colors.text;
|
|
24
25
|
const iconColor = isPrimary ? "#fff" : colors.text;
|
|
26
|
+
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
25
27
|
const on = element.on;
|
|
26
28
|
const showExternalIcon = isExternalLinkAction(on);
|
|
27
|
-
return (_jsx(View, { style: styles.outer, children: _jsxs(Pressable, { style: ({ pressed }) => [
|
|
29
|
+
return (_jsx(View, { style: inHorizontalStack ? styles.outerHorizontal : styles.outer, children: _jsxs(Pressable, { style: ({ pressed }) => [
|
|
28
30
|
styles.btn,
|
|
29
31
|
isPrimary ? styles.btnDefault : styles.btnOther,
|
|
30
32
|
isPrimary
|
|
@@ -50,7 +52,18 @@ export function SnapActionButton({ element, emit, }) {
|
|
|
50
52
|
: null, _jsx(Text, { style: { color: textColor, fontSize: 14, lineHeight: 18, fontWeight: "600" }, children: label }), showExternalIcon ? (_jsx(ExternalLink, { size: 14, color: iconColor, style: { opacity: 0.6 } })) : null] }) }));
|
|
51
53
|
}
|
|
52
54
|
const styles = StyleSheet.create({
|
|
53
|
-
outer: {
|
|
55
|
+
outer: {
|
|
56
|
+
width: "100%",
|
|
57
|
+
minWidth: 0,
|
|
58
|
+
alignSelf: "stretch",
|
|
59
|
+
},
|
|
60
|
+
outerHorizontal: {
|
|
61
|
+
flexGrow: 1,
|
|
62
|
+
flexShrink: 1,
|
|
63
|
+
flexBasis: "auto",
|
|
64
|
+
minWidth: 0,
|
|
65
|
+
alignSelf: "stretch",
|
|
66
|
+
},
|
|
54
67
|
btn: {
|
|
55
68
|
paddingHorizontal: 16,
|
|
56
69
|
borderRadius: 10,
|