@farcaster/snap 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +2 -1
- package/dist/react/components/cell-grid.js +13 -14
- package/dist/react/components/image.js +5 -1
- package/dist/react/components/stack.js +53 -3
- package/dist/react/components/text.js +7 -1
- package/dist/react/stack-direction-context.d.ts +7 -0
- package/dist/react/stack-direction-context.js +10 -0
- package/dist/react/v2/snap-view.js +4 -1
- package/dist/react-native/components/snap-cell-grid.js +5 -7
- package/dist/react-native/components/snap-image.js +15 -2
- package/dist/react-native/components/snap-item.js +12 -2
- package/dist/react-native/components/snap-progress.js +8 -2
- package/dist/react-native/components/snap-stack.d.ts +1 -1
- package/dist/react-native/components/snap-stack.js +85 -10
- package/dist/react-native/components/snap-text.js +7 -2
- package/dist/react-native/stack-direction-context.d.ts +7 -0
- package/dist/react-native/stack-direction-context.js +9 -0
- package/dist/react-native/v2/snap-view.js +2 -1
- package/dist/stack-horizontal-utils.d.ts +4 -0
- package/dist/stack-horizontal-utils.js +29 -0
- package/dist/ui/catalog.d.ts +1 -0
- package/dist/ui/catalog.js +1 -1
- package/dist/ui/stack.d.ts +1 -0
- package/dist/ui/stack.js +8 -0
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/src/constants.ts +2 -1
- package/src/react/components/cell-grid.tsx +17 -24
- package/src/react/components/image.tsx +8 -1
- package/src/react/components/stack.tsx +84 -11
- package/src/react/components/text.tsx +8 -1
- package/src/react/stack-direction-context.tsx +27 -0
- package/src/react/v2/snap-view.tsx +8 -2
- package/src/react-native/components/snap-cell-grid.tsx +5 -11
- package/src/react-native/components/snap-image.tsx +17 -2
- package/src/react-native/components/snap-item.tsx +14 -2
- package/src/react-native/components/snap-progress.tsx +8 -2
- package/src/react-native/components/snap-stack.tsx +116 -14
- package/src/react-native/components/snap-text.tsx +7 -2
- package/src/react-native/stack-direction-context.tsx +25 -0
- package/src/react-native/v2/snap-view.tsx +3 -2
- package/src/stack-horizontal-utils.ts +27 -0
- package/src/ui/catalog.ts +1 -1
- package/src/ui/stack.ts +8 -0
package/dist/constants.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare const GRID_GAP_VALUES: readonly ["none", "sm", "md", "lg"];
|
|
|
14
14
|
export declare const MAX_ELEMENTS = 64;
|
|
15
15
|
export declare const MAX_ROOT_CHILDREN = 7;
|
|
16
16
|
export declare const MAX_CHILDREN = 6;
|
|
17
|
-
|
|
17
|
+
/** Enough depth for side-by-side columns that contain labeled horizontal icon rows (pair → column → row → icon). */
|
|
18
|
+
export declare const MAX_DEPTH = 5;
|
|
18
19
|
export declare const BAR_CHART_MAX_BARS = 6;
|
|
19
20
|
export declare const BAR_CHART_LABEL_MAX_CHARS = 40;
|
package/dist/constants.js
CHANGED
|
@@ -15,7 +15,8 @@ export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"];
|
|
|
15
15
|
export const MAX_ELEMENTS = 64;
|
|
16
16
|
export const MAX_ROOT_CHILDREN = 7;
|
|
17
17
|
export const MAX_CHILDREN = 6;
|
|
18
|
-
|
|
18
|
+
/** Enough depth for side-by-side columns that contain labeled horizontal icon rows (pair → column → row → icon). */
|
|
19
|
+
export const MAX_DEPTH = 5;
|
|
19
20
|
// ─── Bar chart ─────────────────────────────────────────
|
|
20
21
|
export const BAR_CHART_MAX_BARS = 6;
|
|
21
22
|
export const BAR_CHART_LABEL_MAX_CHARS = 40;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useStateStore } from "@json-render/react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
@@ -54,12 +54,14 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
54
54
|
content: c.content != null ? String(c.content) : undefined,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
58
|
+
const emptyCellBg = colors.mode === "dark" ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.05)";
|
|
57
59
|
const cellEls = [];
|
|
58
60
|
for (let r = 0; r < rows; r++) {
|
|
59
61
|
for (let c = 0; c < cols; c++) {
|
|
60
62
|
const cell = cellMap.get(`${r},${c}`);
|
|
61
63
|
const selected = interactive && isSelected(r, c);
|
|
62
|
-
const bg = cell?.color ? colors.colorHex(cell.color) :
|
|
64
|
+
const bg = cell?.color ? colors.colorHex(cell.color) : emptyCellBg;
|
|
63
65
|
cellEls.push(_jsx("div", { role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, onClick: interactive ? () => handleTap(r, c) : undefined, onKeyDown: interactive
|
|
64
66
|
? (e) => {
|
|
65
67
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -76,16 +78,13 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
76
78
|
}, children: cell?.content ?? "" }, `${r}-${c}`));
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
borderRadius: 8,
|
|
89
|
-
backgroundColor: colors.muted,
|
|
90
|
-
}, children: cellEls }), selectionLabel && (_jsx("div", { className: "mt-1.5 truncate text-xs font-mono", style: { color: colors.textMuted }, children: selectionLabel }))] }));
|
|
81
|
+
return (_jsx("div", { style: {
|
|
82
|
+
display: "grid",
|
|
83
|
+
width: "100%",
|
|
84
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
85
|
+
gap: gapPx,
|
|
86
|
+
padding: 4,
|
|
87
|
+
borderRadius: 8,
|
|
88
|
+
backgroundColor: colors.muted,
|
|
89
|
+
}, children: cellEls }));
|
|
91
90
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { AspectRatio } from "@neynar/ui/aspect-ratio";
|
|
4
|
+
import { cn } from "@neynar/ui/utils";
|
|
5
|
+
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
4
6
|
function aspectToRatio(aspect) {
|
|
5
7
|
const [w, h] = aspect.split(":").map(Number);
|
|
6
8
|
if (!w || !h)
|
|
@@ -11,5 +13,7 @@ export function SnapImage({ element: { props }, }) {
|
|
|
11
13
|
const url = String(props.url ?? "");
|
|
12
14
|
const alt = String(props.alt ?? "");
|
|
13
15
|
const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
|
|
14
|
-
|
|
16
|
+
const stackDir = useSnapStackDirection();
|
|
17
|
+
const inHorizontalStack = stackDir === "horizontal";
|
|
18
|
+
return (_jsx(AspectRatio, { ratio: ratio, className: cn("relative overflow-hidden rounded-lg", inHorizontalStack ? "min-w-0 flex-1 basis-0" : "w-full"), children: _jsx("img", { src: url, alt: alt, className: "absolute inset-0 size-full object-cover" }) }));
|
|
15
19
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
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, horizontalChildrenAreAllButtons, } from "../../stack-horizontal-utils.js";
|
|
5
|
+
import { SnapStackDirectionProvider, useSnapStackDirection, } from "../stack-direction-context.js";
|
|
4
6
|
const VGAP = {
|
|
5
7
|
none: "gap-0",
|
|
6
8
|
sm: "gap-2",
|
|
@@ -13,20 +15,68 @@ const HGAP = {
|
|
|
13
15
|
md: "gap-2",
|
|
14
16
|
lg: "gap-3",
|
|
15
17
|
};
|
|
16
|
-
const
|
|
18
|
+
const JUSTIFY_FLEX = {
|
|
17
19
|
start: "justify-start",
|
|
18
20
|
center: "justify-center",
|
|
19
21
|
end: "justify-end",
|
|
20
22
|
between: "justify-between",
|
|
21
23
|
around: "justify-around",
|
|
22
24
|
};
|
|
25
|
+
/** Equal columns for explicit `columns` prop and for all-button horizontal rows. */
|
|
26
|
+
const COLUMN_GRID_CLASS = {
|
|
27
|
+
1: "grid grid-cols-1 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
28
|
+
2: "grid grid-cols-2 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
29
|
+
3: "grid grid-cols-3 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
30
|
+
4: "grid grid-cols-4 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
31
|
+
5: "grid grid-cols-5 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
32
|
+
6: "grid grid-cols-6 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
33
|
+
};
|
|
23
34
|
export function SnapStack({ element: { props }, children, }) {
|
|
35
|
+
const parentDirection = useSnapStackDirection();
|
|
24
36
|
const direction = String(props.direction ?? "vertical");
|
|
25
37
|
const gapKey = String(props.gap ?? "md");
|
|
26
38
|
const isHorizontal = direction === "horizontal";
|
|
27
39
|
const gap = isHorizontal
|
|
28
40
|
? (HGAP[gapKey] ?? "gap-2")
|
|
29
41
|
: (VGAP[gapKey] ?? "gap-4");
|
|
30
|
-
const
|
|
31
|
-
|
|
42
|
+
const justifyKey = props.justify ? String(props.justify) : undefined;
|
|
43
|
+
const justifyFlex = justifyKey ? JUSTIFY_FLEX[justifyKey] : undefined;
|
|
44
|
+
const buttonRowGrid = isHorizontal && horizontalChildrenAreAllButtons(children);
|
|
45
|
+
const buttonRowCount = buttonRowGrid
|
|
46
|
+
? countRenderableChildren(children)
|
|
47
|
+
: 0;
|
|
48
|
+
const columnsRaw = props.columns;
|
|
49
|
+
const columns = typeof columnsRaw === "number" &&
|
|
50
|
+
columnsRaw >= 2 &&
|
|
51
|
+
columnsRaw <= 6 &&
|
|
52
|
+
Number.isInteger(columnsRaw)
|
|
53
|
+
? columnsRaw
|
|
54
|
+
: undefined;
|
|
55
|
+
const explicitColumnGrid = isHorizontal && columns !== undefined && !buttonRowGrid;
|
|
56
|
+
const columnGridClass = explicitColumnGrid && columns !== undefined
|
|
57
|
+
? COLUMN_GRID_CLASS[columns]
|
|
58
|
+
: undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Row peers under a horizontal stack must shrink and share width (`flex-1` + `min-w-0`).
|
|
61
|
+
* Avoid `w-full` here: it resolves to 100% of the flex/grid container and fights peer sizing,
|
|
62
|
+
* so each column stacks on its own wrapped row instead of sitting side-by-side.
|
|
63
|
+
*/
|
|
64
|
+
const isRowChild = parentDirection === "horizontal";
|
|
65
|
+
const rootWidthClass = isRowChild
|
|
66
|
+
? "min-w-0 flex-1 basis-0 max-w-full"
|
|
67
|
+
: "w-full min-w-0";
|
|
68
|
+
const justifyBlockGrid = justifyFlex &&
|
|
69
|
+
(!isHorizontal || (!buttonRowGrid && !explicitColumnGrid));
|
|
70
|
+
/** Single flex row (nowrap): peers stay side-by-side and shrink via min-w-0 / flex-1 on nested stacks. */
|
|
71
|
+
const horizontalFlexClasses = "flex min-w-0 flex-row flex-nowrap items-stretch [&>*]:min-w-0";
|
|
72
|
+
return (_jsx(SnapStackDirectionProvider, { direction: isHorizontal ? "horizontal" : "vertical", children: _jsx("div", { className: cn(rootWidthClass, isHorizontal
|
|
73
|
+
? buttonRowGrid &&
|
|
74
|
+
buttonRowCount >= 1 &&
|
|
75
|
+
buttonRowCount <= 6 &&
|
|
76
|
+
COLUMN_GRID_CLASS[buttonRowCount]
|
|
77
|
+
? cn(COLUMN_GRID_CLASS[buttonRowCount], gap, "[&>*]:w-full")
|
|
78
|
+
: explicitColumnGrid && columnGridClass
|
|
79
|
+
? cn(columnGridClass, gap)
|
|
80
|
+
: cn(horizontalFlexClasses, gap, justifyBlockGrid ? justifyFlex : undefined)
|
|
81
|
+
: cn("flex min-w-0 w-full flex-col", gap, justifyFlex)), children: children }) }));
|
|
32
82
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Text } from "@neynar/ui/typography";
|
|
4
|
+
import { cn } from "@neynar/ui/utils";
|
|
4
5
|
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
6
|
+
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
5
7
|
const SIZE_MAP = {
|
|
6
8
|
md: { textSize: "base" },
|
|
7
9
|
sm: { textSize: "sm" },
|
|
@@ -13,5 +15,9 @@ export function SnapText({ element: { props }, }) {
|
|
|
13
15
|
const align = props.align ?? undefined;
|
|
14
16
|
const config = SIZE_MAP[size] ?? SIZE_MAP.md;
|
|
15
17
|
const colors = useSnapColors();
|
|
16
|
-
|
|
18
|
+
const stackDir = useSnapStackDirection();
|
|
19
|
+
const inHorizontalStack = stackDir === "horizontal";
|
|
20
|
+
return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: cn(
|
|
21
|
+
/** Row peers hug content like RN `wrapRow`; avoid `flex-1` stretching peers across the row. */
|
|
22
|
+
inHorizontalStack ? "min-w-0 shrink" : "flex-1"), style: { color: colors.text }, children: content }));
|
|
17
23
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type SnapStackDirection = "vertical" | "horizontal";
|
|
3
|
+
export declare function SnapStackDirectionProvider({ direction, children, }: {
|
|
4
|
+
direction: SnapStackDirection;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function useSnapStackDirection(): SnapStackDirection | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
const SnapStackDirectionContext = createContext(undefined);
|
|
5
|
+
export function SnapStackDirectionProvider({ direction, children, }) {
|
|
6
|
+
return (_jsx(SnapStackDirectionContext.Provider, { value: direction, children: children }));
|
|
7
|
+
}
|
|
8
|
+
export function useSnapStackDirection() {
|
|
9
|
+
return useContext(SnapStackDirectionContext);
|
|
10
|
+
}
|
|
@@ -6,6 +6,7 @@ import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core.js";
|
|
|
6
6
|
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
|
|
7
7
|
const SNAP_MAX_HEIGHT = 500;
|
|
8
8
|
const SNAP_WARNING_HEIGHT = 700;
|
|
9
|
+
const SHOW_MORE_OVERHANG = 14;
|
|
9
10
|
// ─── Default validation error fallback ────────────────
|
|
10
11
|
function SnapValidationFallback({ appearance, message, }) {
|
|
11
12
|
const isDark = appearance === "dark";
|
|
@@ -83,7 +84,9 @@ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark
|
|
|
83
84
|
}, [isExpandable]);
|
|
84
85
|
const isClipped = !showOverflowWarning && isExpandable && !isExpanded;
|
|
85
86
|
const containerMaxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : undefined;
|
|
86
|
-
return (_jsxs(
|
|
87
|
+
return (_jsxs("div", { style: {
|
|
88
|
+
paddingBottom: !showOverflowWarning && isExpandable ? SHOW_MORE_OVERHANG : 0,
|
|
89
|
+
}, children: [_jsxs("div", { style: {
|
|
87
90
|
position: "relative",
|
|
88
91
|
width: "100%",
|
|
89
92
|
maxWidth,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, Text, View, Pressable } from "react-native";
|
|
3
3
|
import { useStateStore } from "@json-render/react-native";
|
|
4
4
|
import { useSnapPalette } from "../use-snap-palette.js";
|
|
@@ -58,13 +58,15 @@ export function SnapCellGrid({ element, emit, }) {
|
|
|
58
58
|
}
|
|
59
59
|
const ringOuter = appearance === "dark" ? "#fff" : "#000";
|
|
60
60
|
const ringInner = appearance === "dark" ? "#000" : "#fff";
|
|
61
|
+
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
62
|
+
const emptyCellBg = appearance === "dark" ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.05)";
|
|
61
63
|
const rowEls = [];
|
|
62
64
|
for (let r = 0; r < rows; r++) {
|
|
63
65
|
const rowCells = [];
|
|
64
66
|
for (let c = 0; c < cols; c++) {
|
|
65
67
|
const cell = cellMap.get(`${r},${c}`);
|
|
66
68
|
const selected = interactive && isSelected(r, c);
|
|
67
|
-
const bg = cell?.color ? hex(cell.color) :
|
|
69
|
+
const bg = cell?.color ? hex(cell.color) : emptyCellBg;
|
|
68
70
|
const cellContent = cell?.content ? (_jsx(Text, { style: [styles.cellText, { color: colors.textPrimary }], children: cell.content })) : null;
|
|
69
71
|
// Two-tone ring: outer View with contrasting border, inner View with inverse border
|
|
70
72
|
const cellView = selected ? (_jsx(View, { style: [styles.cell, { height: rowHeight, borderWidth: 1, borderColor: ringOuter, borderRadius: 4 }], children: _jsx(View, { style: [
|
|
@@ -75,10 +77,7 @@ export function SnapCellGrid({ element, emit, }) {
|
|
|
75
77
|
}
|
|
76
78
|
rowEls.push(_jsx(View, { style: [styles.gridRow, { gap: gapPx }], children: rowCells }, r));
|
|
77
79
|
}
|
|
78
|
-
|
|
79
|
-
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
80
|
-
: null;
|
|
81
|
-
return (_jsxs(View, { style: [styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }], children: [rowEls, selectionLabel ? (_jsx(Text, { style: [styles.selectionText, { color: colors.textSecondary }], children: selectionLabel })) : null] }));
|
|
80
|
+
return (_jsx(View, { style: [styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }], children: rowEls }));
|
|
82
81
|
}
|
|
83
82
|
const styles = StyleSheet.create({
|
|
84
83
|
wrap: { width: "100%" },
|
|
@@ -96,5 +95,4 @@ const styles = StyleSheet.create({
|
|
|
96
95
|
justifyContent: "center",
|
|
97
96
|
},
|
|
98
97
|
cellText: { fontSize: 12, lineHeight: 16, fontWeight: "600" },
|
|
99
|
-
selectionText: { fontSize: 11, fontFamily: "monospace", marginTop: 6 },
|
|
100
98
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Image } from "expo-image";
|
|
3
3
|
import { StyleSheet, View } from "react-native";
|
|
4
|
+
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
4
5
|
function aspectToRatio(aspect) {
|
|
5
6
|
const [w, h] = aspect.split(":").map(Number);
|
|
6
7
|
if (!w || !h)
|
|
@@ -11,13 +12,25 @@ export function SnapImage({ element: { props }, }) {
|
|
|
11
12
|
const url = String(props.url ?? "");
|
|
12
13
|
const alt = String(props.alt ?? "");
|
|
13
14
|
const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
|
|
14
|
-
|
|
15
|
+
const stackDir = useSnapStackDirection();
|
|
16
|
+
const inHorizontalStack = stackDir === "horizontal";
|
|
17
|
+
return (_jsx(View, { style: [
|
|
18
|
+
styles.frame,
|
|
19
|
+
inHorizontalStack ? styles.frameInHorizontalRow : styles.frameFullWidth,
|
|
20
|
+
{ aspectRatio: ratio },
|
|
21
|
+
], children: _jsx(Image, { source: { uri: url }, style: StyleSheet.absoluteFill, contentFit: "cover", accessibilityLabel: alt || undefined }) }));
|
|
15
22
|
}
|
|
16
23
|
const styles = StyleSheet.create({
|
|
17
24
|
frame: {
|
|
18
|
-
width: "100%",
|
|
19
25
|
borderRadius: 8,
|
|
20
26
|
overflow: "hidden",
|
|
21
27
|
backgroundColor: "#f3f4f6",
|
|
22
28
|
},
|
|
29
|
+
frameFullWidth: {
|
|
30
|
+
width: "100%",
|
|
31
|
+
},
|
|
32
|
+
frameInHorizontalRow: {
|
|
33
|
+
flex: 1,
|
|
34
|
+
minWidth: 0,
|
|
35
|
+
},
|
|
23
36
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
3
4
|
import { useSnapTheme } from "../theme.js";
|
|
4
5
|
export function SnapItem({ element: { props }, children, }) {
|
|
5
6
|
const { colors } = useSnapTheme();
|
|
@@ -7,15 +8,24 @@ export function SnapItem({ element: { props }, children, }) {
|
|
|
7
8
|
const description = props.description
|
|
8
9
|
? String(props.description)
|
|
9
10
|
: undefined;
|
|
10
|
-
|
|
11
|
+
/** Match web `Item className="flex-1"`: row peers must share width or title/description collapse. */
|
|
12
|
+
const rowPeer = useSnapStackDirection() === "horizontal";
|
|
11
13
|
const containerVariant = { paddingVertical: 6, paddingHorizontal: 10 };
|
|
12
|
-
return (_jsxs(View, { style: [
|
|
14
|
+
return (_jsxs(View, { style: [
|
|
15
|
+
styles.container,
|
|
16
|
+
containerVariant,
|
|
17
|
+
rowPeer && styles.rowPeer,
|
|
18
|
+
], children: [_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] }));
|
|
13
19
|
}
|
|
14
20
|
const styles = StyleSheet.create({
|
|
15
21
|
container: {
|
|
16
22
|
flexDirection: "row",
|
|
17
23
|
alignItems: "center",
|
|
18
24
|
},
|
|
25
|
+
rowPeer: {
|
|
26
|
+
flex: 1,
|
|
27
|
+
minWidth: 0,
|
|
28
|
+
},
|
|
19
29
|
content: {
|
|
20
30
|
flex: 1,
|
|
21
31
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
3
4
|
import { useSnapPalette } from "../use-snap-palette.js";
|
|
4
5
|
import { useSnapTheme } from "../theme.js";
|
|
5
6
|
export function SnapProgress({ element: { props }, }) {
|
|
@@ -9,10 +10,15 @@ export function SnapProgress({ element: { props }, }) {
|
|
|
9
10
|
const max = Math.max(1, Number(props.max ?? 100));
|
|
10
11
|
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
11
12
|
const label = props.label != null ? String(props.label) : null;
|
|
12
|
-
|
|
13
|
+
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
14
|
+
return (_jsxs(View, { style: [styles.wrap, inHorizontalStack ? styles.wrapRowPeer : styles.wrapCol], children: [label ? (_jsx(Text, { style: [styles.label, { color: colors.textSecondary }], children: label })) : null, _jsx(View, { style: [styles.track, { backgroundColor: colors.muted }], children: _jsx(View, { style: [styles.fill, { width: `${percent}%`, backgroundColor: accentHex }] }) })] }));
|
|
13
15
|
}
|
|
14
16
|
const styles = StyleSheet.create({
|
|
15
|
-
wrap: {
|
|
17
|
+
wrap: { gap: 4 },
|
|
18
|
+
/** Vertical stacks: span card width (matches web `w-full`). */
|
|
19
|
+
wrapCol: { width: "100%" },
|
|
20
|
+
/** Horizontal row peers: share space; `width: 100%` each overflows the row. */
|
|
21
|
+
wrapRowPeer: { flex: 1, minWidth: 0 },
|
|
16
22
|
label: { fontSize: 13, lineHeight: 18 },
|
|
17
23
|
track: {
|
|
18
24
|
height: 10,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
|
-
import type
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
3
|
export declare function SnapStack({ element: { props }, children, }: ComponentRenderProps<Record<string, unknown>> & {
|
|
4
4
|
children?: ReactNode;
|
|
5
5
|
}): import("react").JSX.Element;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Children } from "react";
|
|
2
3
|
import { StyleSheet, View } from "react-native";
|
|
4
|
+
import { countRenderableChildren, horizontalChildrenAreAllButtons, } from "../../stack-horizontal-utils.js";
|
|
5
|
+
import { SnapStackDirectionProvider, useSnapStackDirection, } from "../stack-direction-context.js";
|
|
3
6
|
const VGAP = {
|
|
4
7
|
none: 0,
|
|
5
8
|
sm: 8,
|
|
@@ -19,7 +22,13 @@ const JUSTIFY = {
|
|
|
19
22
|
between: "space-between",
|
|
20
23
|
around: "space-around",
|
|
21
24
|
};
|
|
25
|
+
/** Equal-width cells for explicit `columns` and all-button horizontal rows. */
|
|
26
|
+
function wrapEqualColumnCells(children) {
|
|
27
|
+
const cells = Children.toArray(children).filter((c) => c != null && c !== false);
|
|
28
|
+
return cells.map((child, i) => (_jsx(View, { style: styles.equalColumnCell, children: child }, i)));
|
|
29
|
+
}
|
|
22
30
|
export function SnapStack({ element: { props }, children, }) {
|
|
31
|
+
const parentDirection = useSnapStackDirection();
|
|
23
32
|
const direction = String(props.direction ?? "vertical");
|
|
24
33
|
const rawGap = props.gap;
|
|
25
34
|
const isHorizontal = direction === "horizontal";
|
|
@@ -29,21 +38,87 @@ export function SnapStack({ element: { props }, children, }) {
|
|
|
29
38
|
: typeof rawGap === "string" && rawGap in gapMap
|
|
30
39
|
? gapMap[rawGap]
|
|
31
40
|
: isHorizontal ? HGAP.md : VGAP.md;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
const buttonRowGrid = isHorizontal && horizontalChildrenAreAllButtons(children);
|
|
42
|
+
const buttonRowCount = buttonRowGrid
|
|
43
|
+
? countRenderableChildren(children)
|
|
44
|
+
: 0;
|
|
45
|
+
const columnsRaw = props.columns;
|
|
46
|
+
const columns = typeof columnsRaw === "number" &&
|
|
47
|
+
columnsRaw >= 2 &&
|
|
48
|
+
columnsRaw <= 6 &&
|
|
49
|
+
Number.isInteger(columnsRaw)
|
|
50
|
+
? columnsRaw
|
|
51
|
+
: undefined;
|
|
52
|
+
const explicitColumnGrid = isHorizontal && columns !== undefined && !buttonRowGrid;
|
|
53
|
+
const justify = props.justify &&
|
|
54
|
+
(!isHorizontal || (!buttonRowGrid && !explicitColumnGrid))
|
|
55
|
+
? JUSTIFY[String(props.justify)]
|
|
56
|
+
: undefined;
|
|
57
|
+
const isRowChild = parentDirection === "horizontal";
|
|
58
|
+
const packedHorizontal = isHorizontal &&
|
|
59
|
+
((buttonRowGrid &&
|
|
60
|
+
buttonRowCount >= 1 &&
|
|
61
|
+
buttonRowCount <= 6) ||
|
|
62
|
+
explicitColumnGrid);
|
|
63
|
+
let horizontalBody = children;
|
|
64
|
+
if (isHorizontal &&
|
|
65
|
+
buttonRowGrid &&
|
|
66
|
+
buttonRowCount >= 1 &&
|
|
67
|
+
buttonRowCount <= 6) {
|
|
68
|
+
horizontalBody = wrapEqualColumnCells(children);
|
|
69
|
+
}
|
|
70
|
+
else if (isHorizontal && explicitColumnGrid && columns !== undefined) {
|
|
71
|
+
horizontalBody = wrapEqualColumnCells(children);
|
|
72
|
+
}
|
|
73
|
+
return (_jsx(SnapStackDirectionProvider, { direction: isHorizontal ? "horizontal" : "vertical", children: _jsx(View, { style: [
|
|
74
|
+
isRowChild ? styles.stackRowChild : styles.stack,
|
|
75
|
+
isHorizontal
|
|
76
|
+
? packedHorizontal
|
|
77
|
+
? styles.horizontalPacked
|
|
78
|
+
: styles.horizontalDefault
|
|
79
|
+
: styles.verticalStack,
|
|
80
|
+
{ gap },
|
|
81
|
+
justify ? { justifyContent: justify } : undefined,
|
|
82
|
+
], children: horizontalBody }) }));
|
|
39
83
|
}
|
|
40
84
|
const styles = StyleSheet.create({
|
|
41
85
|
stack: {
|
|
42
86
|
width: "100%",
|
|
87
|
+
minWidth: 0,
|
|
88
|
+
},
|
|
89
|
+
verticalStack: {
|
|
90
|
+
width: "100%",
|
|
91
|
+
minWidth: 0,
|
|
43
92
|
},
|
|
44
|
-
horizontal
|
|
93
|
+
/** Nested stack inside a horizontal row — share width with siblings (matches web flex peers). */
|
|
94
|
+
stackRowChild: {
|
|
95
|
+
flexGrow: 1,
|
|
96
|
+
flexShrink: 1,
|
|
97
|
+
flexBasis: 0,
|
|
98
|
+
minWidth: 0,
|
|
99
|
+
maxWidth: "100%",
|
|
100
|
+
alignSelf: "stretch",
|
|
101
|
+
},
|
|
102
|
+
/** Default horizontal row: single line, equal-height peers. */
|
|
103
|
+
horizontalDefault: {
|
|
45
104
|
flexDirection: "row",
|
|
46
|
-
alignItems: "
|
|
47
|
-
flexWrap: "
|
|
105
|
+
alignItems: "stretch",
|
|
106
|
+
flexWrap: "nowrap",
|
|
107
|
+
width: "100%",
|
|
108
|
+
minWidth: 0,
|
|
109
|
+
},
|
|
110
|
+
/** Single row for packed equal-width cells (button grids & explicit columns). */
|
|
111
|
+
horizontalPacked: {
|
|
112
|
+
flexDirection: "row",
|
|
113
|
+
flexWrap: "nowrap",
|
|
114
|
+
alignItems: "stretch",
|
|
115
|
+
width: "100%",
|
|
116
|
+
minWidth: 0,
|
|
117
|
+
},
|
|
118
|
+
equalColumnCell: {
|
|
119
|
+
flexGrow: 1,
|
|
120
|
+
flexShrink: 1,
|
|
121
|
+
flexBasis: 0,
|
|
122
|
+
minWidth: 0,
|
|
48
123
|
},
|
|
49
124
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
3
4
|
import { useSnapTheme } from "../theme.js";
|
|
4
5
|
const SIZE_STYLES = {
|
|
5
6
|
md: { fontSize: 16, lineHeight: 24 },
|
|
@@ -18,7 +19,8 @@ export function SnapText({ element: { props }, }) {
|
|
|
18
19
|
const sizeStyle = SIZE_STYLES[size] ?? SIZE_STYLES.md;
|
|
19
20
|
const resolvedWeight = weight ? WEIGHT_MAP[weight] : sizeStyle?.fontWeight;
|
|
20
21
|
const textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
|
|
21
|
-
|
|
22
|
+
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
23
|
+
return (_jsx(View, { style: inHorizontalStack ? styles.wrapRow : styles.wrapCol, children: _jsx(Text, { style: [
|
|
22
24
|
styles.base,
|
|
23
25
|
{
|
|
24
26
|
color: colors.text,
|
|
@@ -30,6 +32,9 @@ export function SnapText({ element: { props }, }) {
|
|
|
30
32
|
], children: content }) }));
|
|
31
33
|
}
|
|
32
34
|
const styles = StyleSheet.create({
|
|
33
|
-
|
|
35
|
+
/** Full width for vertical stacks (alignment / wrapping). */
|
|
36
|
+
wrapCol: { width: "100%" },
|
|
37
|
+
/** Row peers: hug content; avoid width 100% fighting nowrap horizontal rows. */
|
|
38
|
+
wrapRow: { flexShrink: 1, minWidth: 0 },
|
|
34
39
|
base: {},
|
|
35
40
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type SnapStackDirection = "vertical" | "horizontal";
|
|
3
|
+
export declare function SnapStackDirectionProvider({ direction, children, }: {
|
|
4
|
+
direction: SnapStackDirection;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function useSnapStackDirection(): SnapStackDirection | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const SnapStackDirectionContext = createContext(undefined);
|
|
4
|
+
export function SnapStackDirectionProvider({ direction, children, }) {
|
|
5
|
+
return (_jsx(SnapStackDirectionContext.Provider, { value: direction, children: children }));
|
|
6
|
+
}
|
|
7
|
+
export function useSnapStackDirection() {
|
|
8
|
+
return useContext(SnapStackDirectionContext);
|
|
9
|
+
}
|
|
@@ -7,6 +7,7 @@ import { validateSnapResponse, } from "@farcaster/snap";
|
|
|
7
7
|
// ─── Constants ───────────────────────────────────────
|
|
8
8
|
const SNAP_MAX_HEIGHT = 500;
|
|
9
9
|
const SNAP_WARNING_HEIGHT = 700;
|
|
10
|
+
const SHOW_MORE_OVERHANG = 14;
|
|
10
11
|
// ─── Validation fallback ─────────────────────────────
|
|
11
12
|
function SnapValidationFallback({ message }) {
|
|
12
13
|
const { colors } = useSnapTheme();
|
|
@@ -85,7 +86,7 @@ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWa
|
|
|
85
86
|
const isDark = mode === "dark";
|
|
86
87
|
const pillBg = isDark ? "rgba(40,40,40,0.92)" : "rgba(255,255,255,0.92)";
|
|
87
88
|
const pillBgPressed = isDark ? "rgba(60,60,60,0.95)" : "rgba(240,240,240,0.95)";
|
|
88
|
-
return (_jsxs(
|
|
89
|
+
return (_jsxs(View, { style: { paddingBottom: isExpandable ? SHOW_MORE_OVERHANG : 0 }, children: [_jsxs(View, { style: { position: "relative" }, children: [_jsxs(View, { style: {
|
|
89
90
|
borderRadius,
|
|
90
91
|
borderWidth: 1,
|
|
91
92
|
borderColor: colors.border,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export declare function horizontalChildrenAreAllButtons(children: ReactNode): boolean;
|
|
3
|
+
/** Direct snap catalog children under a stack (used for all-button grid column count). */
|
|
4
|
+
export declare function countRenderableChildren(children: ReactNode): number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Children, isValidElement } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* True when every rendered child comes from a catalog `button` element.
|
|
4
|
+
* json-render passes `{ element: { type, props, ... } }` into each catalog component.
|
|
5
|
+
*/
|
|
6
|
+
function isRenderableChild(c) {
|
|
7
|
+
if (c == null)
|
|
8
|
+
return false;
|
|
9
|
+
if (typeof c === "boolean")
|
|
10
|
+
return false;
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
export function horizontalChildrenAreAllButtons(children) {
|
|
14
|
+
const items = Children.toArray(children).filter(isRenderableChild);
|
|
15
|
+
if (items.length === 0)
|
|
16
|
+
return false;
|
|
17
|
+
for (const child of items) {
|
|
18
|
+
if (!isValidElement(child))
|
|
19
|
+
return false;
|
|
20
|
+
const typ = child.props.element?.type;
|
|
21
|
+
if (typ !== "button")
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/** Direct snap catalog children under a stack (used for all-button grid column count). */
|
|
27
|
+
export function countRenderableChildren(children) {
|
|
28
|
+
return Children.toArray(children).filter(isRenderableChild).length;
|
|
29
|
+
}
|
package/dist/ui/catalog.d.ts
CHANGED
|
@@ -318,6 +318,7 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
318
318
|
between: "between";
|
|
319
319
|
around: "around";
|
|
320
320
|
}>>;
|
|
321
|
+
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
|
|
321
322
|
}, z.core.$strip>;
|
|
322
323
|
description: string;
|
|
323
324
|
};
|
package/dist/ui/catalog.js
CHANGED
|
@@ -78,7 +78,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
78
78
|
},
|
|
79
79
|
stack: {
|
|
80
80
|
props: stackProps,
|
|
81
|
-
description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order.",
|
|
81
|
+
description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order. Horizontal stacks use a single flex row so peers stay side-by-side and shrink with min-width 0. Nested stacks participate as flexible row peers. All-button horizontal stacks use an equal N-column grid where N is the number of buttons (1–6). Optional `columns` (`2`–`6`) forces an explicit equal grid for mixed children.",
|
|
82
82
|
},
|
|
83
83
|
text: {
|
|
84
84
|
props: textProps,
|
package/dist/ui/stack.d.ts
CHANGED
|
@@ -20,5 +20,6 @@ export declare const stackProps: z.ZodObject<{
|
|
|
20
20
|
between: "between";
|
|
21
21
|
around: "around";
|
|
22
22
|
}>>;
|
|
23
|
+
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
|
|
23
24
|
}, z.core.$strip>;
|
|
24
25
|
export type StackProps = z.infer<typeof stackProps>;
|