@farcaster/snap 2.4.0 → 2.5.1
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 +1 -0
- package/dist/constants.js +1 -0
- package/dist/react/components/action-button.js +5 -5
- package/dist/react/components/cell-grid.js +12 -3
- 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-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 +36 -5
- 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/stack-horizontal-utils.d.ts +7 -5
- package/dist/stack-horizontal-utils.js +24 -14
- package/dist/ui/catalog.d.ts +70 -1
- package/dist/ui/catalog.js +4 -4
- package/dist/ui/cell-grid.d.ts +14 -0
- package/dist/ui/cell-grid.js +7 -7
- 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 +1 -0
- package/src/react/components/action-button.tsx +5 -4
- package/src/react/components/cell-grid.tsx +13 -4
- 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-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 +43 -6
- 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/stack-horizontal-utils.ts +32 -13
- package/src/ui/README.md +1 -1
- package/src/ui/catalog.ts +6 -5
- package/src/ui/cell-grid.ts +8 -7
- 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
|
@@ -12,6 +12,7 @@ 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
|
@@ -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);
|
|
@@ -26,6 +27,7 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
26
27
|
for (const c of cells) {
|
|
27
28
|
cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
|
|
28
29
|
color: c.color,
|
|
30
|
+
textColor: c.textColor,
|
|
29
31
|
content: c.content != null ? String(c.content) : undefined,
|
|
30
32
|
value: typeof c.value === "string" ? c.value : undefined,
|
|
31
33
|
});
|
|
@@ -63,7 +65,9 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
63
65
|
emit("press");
|
|
64
66
|
};
|
|
65
67
|
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
66
|
-
const emptyCellBg = colors.mode === "dark"
|
|
68
|
+
const emptyCellBg = colors.mode === "dark"
|
|
69
|
+
? "rgba(255, 255, 255, 0.05)"
|
|
70
|
+
: "rgba(0, 0, 0, 0.05)";
|
|
67
71
|
const cellEls = [];
|
|
68
72
|
for (let r = 0; r < rows; r++) {
|
|
69
73
|
for (let c = 0; c < cols; c++) {
|
|
@@ -71,7 +75,11 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
71
75
|
const selected = interactive && isSelected(r, c);
|
|
72
76
|
const bgHex = cell?.color ? colors.colorHex(cell.color) : null;
|
|
73
77
|
const bg = bgHex ?? emptyCellBg;
|
|
74
|
-
const textColor =
|
|
78
|
+
const textColor = cell?.textColor
|
|
79
|
+
? colors.colorHex(cell.textColor)
|
|
80
|
+
: bgHex
|
|
81
|
+
? readableTextOnHex(bgHex)
|
|
82
|
+
: colors.text;
|
|
75
83
|
cellEls.push(_jsx("div", { role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, onClick: interactive ? () => handleTap(r, c) : undefined, onKeyDown: interactive
|
|
76
84
|
? (e) => {
|
|
77
85
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -80,7 +88,8 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
: undefined, className: cn("flex items-center justify-center rounded text-xs font-semibold", interactive ? "cursor-pointer select-none" : "cursor-default"), style: {
|
|
83
|
-
height: rowHeight,
|
|
91
|
+
height: squareCells ? undefined : rowHeight,
|
|
92
|
+
aspectRatio: squareCells ? "1 / 1" : undefined,
|
|
84
93
|
background: bg,
|
|
85
94
|
color: textColor,
|
|
86
95
|
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
|
}
|
|
@@ -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,
|
|
@@ -14,6 +14,7 @@ export function SnapCellGrid({ element, emit, }) {
|
|
|
14
14
|
const rows = Number(props.rows ?? 2);
|
|
15
15
|
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
16
16
|
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
17
|
+
const squareCells = props.cellAspectRatio === "square";
|
|
17
18
|
const gap = String(props.gap ?? "sm");
|
|
18
19
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
19
20
|
const gapPx = gapMap[gap] ?? 1;
|
|
@@ -29,6 +30,7 @@ export function SnapCellGrid({ element, emit, }) {
|
|
|
29
30
|
for (const c of cells) {
|
|
30
31
|
cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
|
|
31
32
|
color: c.color,
|
|
33
|
+
textColor: c.textColor,
|
|
32
34
|
content: c.content != null ? String(c.content) : undefined,
|
|
33
35
|
value: typeof c.value === "string" ? c.value : undefined,
|
|
34
36
|
});
|
|
@@ -77,18 +79,43 @@ export function SnapCellGrid({ element, emit, }) {
|
|
|
77
79
|
const selected = interactive && isSelected(r, c);
|
|
78
80
|
const bgHex = cell?.color ? hex(cell.color) : null;
|
|
79
81
|
const bg = bgHex ?? emptyCellBg;
|
|
80
|
-
const textColor =
|
|
82
|
+
const textColor = cell?.textColor
|
|
83
|
+
? hex(cell.textColor)
|
|
84
|
+
: bgHex
|
|
85
|
+
? readableTextOnHex(bgHex)
|
|
86
|
+
: colors.text;
|
|
81
87
|
const cellContent = cell?.content ? (_jsx(Text, { style: [styles.cellText, { color: textColor }], children: cell.content })) : null;
|
|
82
88
|
// Two-tone ring: outer View with contrasting border, inner View with inverse border
|
|
83
|
-
const cellView = selected ? (_jsx(View, { style: [
|
|
89
|
+
const cellView = selected ? (_jsx(View, { style: [
|
|
90
|
+
styles.cell,
|
|
91
|
+
squareCells ? styles.squareCell : { height: rowHeight },
|
|
92
|
+
{ borderWidth: 1, borderColor: ringOuter, borderRadius: 4 },
|
|
93
|
+
], children: _jsx(View, { style: [
|
|
84
94
|
styles.innerCell,
|
|
85
|
-
{
|
|
86
|
-
|
|
95
|
+
{
|
|
96
|
+
backgroundColor: bg,
|
|
97
|
+
borderWidth: 1,
|
|
98
|
+
borderColor: ringInner,
|
|
99
|
+
borderRadius: 3,
|
|
100
|
+
},
|
|
101
|
+
], children: cellContent }) })) : (_jsx(View, { style: [
|
|
102
|
+
styles.cell,
|
|
103
|
+
squareCells ? styles.squareCell : { height: rowHeight },
|
|
104
|
+
{ backgroundColor: bg },
|
|
105
|
+
], children: cellContent }));
|
|
87
106
|
rowCells.push(interactive ? (_jsx(Pressable, { onPress: () => handleTap(r, c), style: styles.cellWrap, children: cellView }, `${r}-${c}`)) : (_jsx(View, { style: styles.cellWrap, children: cellView }, `${r}-${c}`)));
|
|
88
107
|
}
|
|
89
108
|
rowEls.push(_jsx(View, { style: [styles.gridRow, { gap: gapPx }], children: rowCells }, r));
|
|
90
109
|
}
|
|
91
|
-
return (_jsx(View, { style: [
|
|
110
|
+
return (_jsx(View, { style: [
|
|
111
|
+
styles.wrap,
|
|
112
|
+
{
|
|
113
|
+
gap: gapPx,
|
|
114
|
+
backgroundColor: colors.muted,
|
|
115
|
+
padding: 4,
|
|
116
|
+
borderRadius: 8,
|
|
117
|
+
},
|
|
118
|
+
], children: rowEls }));
|
|
92
119
|
}
|
|
93
120
|
const styles = StyleSheet.create({
|
|
94
121
|
wrap: { width: "100%" },
|
|
@@ -99,6 +126,10 @@ const styles = StyleSheet.create({
|
|
|
99
126
|
alignItems: "center",
|
|
100
127
|
justifyContent: "center",
|
|
101
128
|
},
|
|
129
|
+
squareCell: {
|
|
130
|
+
aspectRatio: 1,
|
|
131
|
+
width: "100%",
|
|
132
|
+
},
|
|
102
133
|
innerCell: {
|
|
103
134
|
width: "100%",
|
|
104
135
|
height: "100%",
|
|
@@ -2,18 +2,28 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Children, Fragment } from "react";
|
|
3
3
|
import { StyleSheet, View } from "react-native";
|
|
4
4
|
import { useSnapTheme } from "../theme.js";
|
|
5
|
-
|
|
5
|
+
import { SnapItemGroupBorderProvider } from "./item-layout-context.js";
|
|
6
|
+
const GAP_MAP = {
|
|
7
|
+
none: 0,
|
|
8
|
+
sm: 4,
|
|
9
|
+
md: 8,
|
|
10
|
+
lg: 12,
|
|
11
|
+
};
|
|
6
12
|
export function SnapItemGroup({ element: { props }, children, }) {
|
|
7
13
|
const { colors } = useSnapTheme();
|
|
8
14
|
const border = Boolean(props.border);
|
|
9
15
|
const separator = Boolean(props.separator);
|
|
10
16
|
const gap = GAP_MAP[String(props.gap ?? "sm")] ?? 4;
|
|
11
17
|
const items = Children.toArray(children);
|
|
12
|
-
return (_jsx(View, { style: [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
return (_jsx(SnapItemGroupBorderProvider, { value: border, children: _jsx(View, { style: [
|
|
19
|
+
styles.group,
|
|
20
|
+
border && {
|
|
21
|
+
borderWidth: 1,
|
|
22
|
+
borderColor: colors.border,
|
|
23
|
+
borderRadius: 12,
|
|
24
|
+
},
|
|
25
|
+
{ gap },
|
|
26
|
+
], children: items.map((child, i) => (_jsxs(Fragment, { children: [separator && i > 0 && (_jsx(View, { style: { height: 1, backgroundColor: colors.border + "80" } })), child] }, i))) }) }));
|
|
17
27
|
}
|
|
18
28
|
const styles = StyleSheet.create({
|
|
19
29
|
group: {
|
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Image } from "expo-image";
|
|
2
3
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
4
|
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
4
5
|
import { useSnapTheme } from "../theme.js";
|
|
6
|
+
import { useSnapPalette } from "../use-snap-palette.js";
|
|
7
|
+
import { useSnapItemGroupHasBorder } from "./item-layout-context.js";
|
|
8
|
+
import { ICON_MAP } from "./snap-icon.js";
|
|
9
|
+
function parseItemMedia(value) {
|
|
10
|
+
if (!value || typeof value !== "object")
|
|
11
|
+
return undefined;
|
|
12
|
+
const media = value;
|
|
13
|
+
if (media.variant === "icon" && typeof media.name === "string") {
|
|
14
|
+
return {
|
|
15
|
+
variant: "icon",
|
|
16
|
+
name: media.name,
|
|
17
|
+
color: typeof media.color === "string" ? media.color : undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (media.variant === "image" && typeof media.url === "string") {
|
|
21
|
+
return {
|
|
22
|
+
variant: "image",
|
|
23
|
+
url: media.url,
|
|
24
|
+
alt: typeof media.alt === "string" ? media.alt : undefined,
|
|
25
|
+
round: typeof media.round === "boolean" ? media.round : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
5
30
|
export function SnapItem({ element: { props }, children, }) {
|
|
6
31
|
const { colors } = useSnapTheme();
|
|
32
|
+
const { accentHex, hex } = useSnapPalette();
|
|
7
33
|
const title = String(props.title ?? "");
|
|
8
|
-
const description = props.description
|
|
9
|
-
|
|
10
|
-
|
|
34
|
+
const description = props.description ? String(props.description) : undefined;
|
|
35
|
+
const media = parseItemMedia(props.media);
|
|
36
|
+
const inBorderedGroup = useSnapItemGroupHasBorder();
|
|
11
37
|
/** Match web `Item className="flex-1"`: row peers must share width or title/description collapse. */
|
|
12
38
|
const rowPeer = useSnapStackDirection() === "horizontal";
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
const MediaIcon = media?.variant === "icon" ? ICON_MAP[media.name] : undefined;
|
|
40
|
+
const mediaColor = media?.variant === "icon" && media.color && media.color !== "accent"
|
|
41
|
+
? hex(media.color)
|
|
42
|
+
: accentHex;
|
|
43
|
+
const containerVariant = {
|
|
44
|
+
paddingVertical: 6,
|
|
45
|
+
paddingHorizontal: inBorderedGroup ? 8 : 0,
|
|
46
|
+
columnGap: 8,
|
|
47
|
+
};
|
|
48
|
+
return (_jsxs(View, { style: [styles.container, containerVariant, rowPeer && styles.rowPeer], children: [media?.variant === "icon" && MediaIcon ? (_jsx(View, { style: styles.iconMedia, children: _jsx(MediaIcon, { size: 20, color: mediaColor }) })) : null, media?.variant === "image" ? (_jsx(View, { style: [styles.imageMedia, media.round && styles.roundImage], children: _jsx(Image, { source: { uri: media.url }, style: StyleSheet.absoluteFill, contentFit: "cover", accessibilityLabel: media.alt || undefined }) })) : null, _jsxs(View, { style: styles.content, children: [title ? (_jsx(Text, { style: [styles.title, { color: colors.text }], children: title })) : null, description ? (_jsx(Text, { style: [styles.description, { color: colors.textSecondary }], children: description })) : null] }), children ? (_jsx(View, { style: styles.actions, children: _jsx(View, { style: { flex: 0 }, children: children }) })) : null] }));
|
|
19
49
|
}
|
|
20
50
|
const styles = StyleSheet.create({
|
|
21
51
|
container: {
|
|
@@ -29,19 +59,32 @@ const styles = StyleSheet.create({
|
|
|
29
59
|
content: {
|
|
30
60
|
flex: 1,
|
|
31
61
|
},
|
|
62
|
+
iconMedia: {
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
},
|
|
66
|
+
imageMedia: {
|
|
67
|
+
width: 40,
|
|
68
|
+
height: 40,
|
|
69
|
+
borderRadius: 6,
|
|
70
|
+
overflow: "hidden",
|
|
71
|
+
backgroundColor: "#f3f4f6",
|
|
72
|
+
},
|
|
73
|
+
roundImage: {
|
|
74
|
+
borderRadius: 9999,
|
|
75
|
+
},
|
|
32
76
|
title: {
|
|
33
77
|
fontSize: 15,
|
|
34
78
|
lineHeight: 20,
|
|
35
79
|
fontWeight: "500",
|
|
36
80
|
},
|
|
37
81
|
description: {
|
|
38
|
-
fontSize:
|
|
39
|
-
lineHeight:
|
|
40
|
-
marginTop: 1,
|
|
82
|
+
fontSize: 12,
|
|
83
|
+
lineHeight: 16,
|
|
41
84
|
},
|
|
42
85
|
actions: {
|
|
43
86
|
marginLeft: "auto",
|
|
44
|
-
paddingLeft:
|
|
87
|
+
paddingLeft: 8,
|
|
45
88
|
flexDirection: "row",
|
|
46
89
|
alignItems: "center",
|
|
47
90
|
flexShrink: 0,
|