@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/ui/stack.js
CHANGED
|
@@ -6,4 +6,12 @@ export const stackProps = z.object({
|
|
|
6
6
|
direction: z.enum(STACK_DIRECTIONS).optional(),
|
|
7
7
|
gap: z.enum(STACK_GAPS).optional(),
|
|
8
8
|
justify: z.enum(STACK_JUSTIFY).optional(),
|
|
9
|
+
/** Horizontal stacks only: fixed column grid (`2`–`6`). Prefer omitting this when children are stacks — they flex as row peers automatically. */
|
|
10
|
+
columns: z.union([
|
|
11
|
+
z.literal(2),
|
|
12
|
+
z.literal(3),
|
|
13
|
+
z.literal(4),
|
|
14
|
+
z.literal(5),
|
|
15
|
+
z.literal(6),
|
|
16
|
+
]).optional(),
|
|
9
17
|
});
|
package/llms.txt
CHANGED
|
@@ -37,7 +37,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
|
|
|
37
37
|
| Total elements | Max **64** in `ui.elements` |
|
|
38
38
|
| Root children | Max **7** children on the root element |
|
|
39
39
|
| Children per element | Max **6** per non-root container (`stack`, `item_group`) |
|
|
40
|
-
| Nesting depth | Max **
|
|
40
|
+
| Nesting depth | Max **5** levels from root to deepest leaf |
|
|
41
41
|
|
|
42
42
|
## Components (16 total)
|
|
43
43
|
|
|
@@ -107,6 +107,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
|
|
|
107
107
|
- `direction` (optional): `"vertical"` | `"horizontal"`. Default: `"vertical"`
|
|
108
108
|
- `gap` (optional): `"none"` | `"sm"` | `"md"` | `"lg"`. Default: `"md"`
|
|
109
109
|
- `justify` (optional): `"start"` | `"center"` | `"end"` | `"between"` | `"around"`
|
|
110
|
+
- `columns` (optional, horizontal only): `2`–`6` — CSS grid with equal columns (mixed children or layout that needs fixed column counts).
|
|
110
111
|
- Children are element IDs
|
|
111
112
|
|
|
112
113
|
**item_group** — Groups item children.
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -20,7 +20,8 @@ export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"] as const;
|
|
|
20
20
|
export const MAX_ELEMENTS = 64;
|
|
21
21
|
export const MAX_ROOT_CHILDREN = 7;
|
|
22
22
|
export const MAX_CHILDREN = 6;
|
|
23
|
-
|
|
23
|
+
/** Enough depth for side-by-side columns that contain labeled horizontal icon rows (pair → column → row → icon). */
|
|
24
|
+
export const MAX_DEPTH = 5;
|
|
24
25
|
|
|
25
26
|
// ─── Bar chart ─────────────────────────────────────────
|
|
26
27
|
export const BAR_CHART_MAX_BARS = 6;
|
|
@@ -64,12 +64,16 @@ export function SnapCellGrid({
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
68
|
+
const emptyCellBg =
|
|
69
|
+
colors.mode === "dark" ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.05)";
|
|
70
|
+
|
|
67
71
|
const cellEls: ReactNode[] = [];
|
|
68
72
|
for (let r = 0; r < rows; r++) {
|
|
69
73
|
for (let c = 0; c < cols; c++) {
|
|
70
74
|
const cell = cellMap.get(`${r},${c}`);
|
|
71
75
|
const selected = interactive && isSelected(r, c);
|
|
72
|
-
const bg = cell?.color ? colors.colorHex(cell.color) :
|
|
76
|
+
const bg = cell?.color ? colors.colorHex(cell.color) : emptyCellBg;
|
|
73
77
|
|
|
74
78
|
cellEls.push(
|
|
75
79
|
<div
|
|
@@ -105,30 +109,19 @@ export function SnapCellGrid({
|
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
109
|
-
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
110
|
-
: null;
|
|
111
|
-
|
|
112
112
|
return (
|
|
113
|
-
<div
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{cellEls}
|
|
126
|
-
</div>
|
|
127
|
-
{selectionLabel && (
|
|
128
|
-
<div className="mt-1.5 truncate text-xs font-mono" style={{ color: colors.textMuted }}>
|
|
129
|
-
{selectionLabel}
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
113
|
+
<div
|
|
114
|
+
style={{
|
|
115
|
+
display: "grid",
|
|
116
|
+
width: "100%",
|
|
117
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
118
|
+
gap: gapPx,
|
|
119
|
+
padding: 4,
|
|
120
|
+
borderRadius: 8,
|
|
121
|
+
backgroundColor: colors.muted,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{cellEls}
|
|
132
125
|
</div>
|
|
133
126
|
);
|
|
134
127
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { AspectRatio } from "@neynar/ui/aspect-ratio";
|
|
4
|
+
import { cn } from "@neynar/ui/utils";
|
|
5
|
+
import { useSnapStackDirection } from "../stack-direction-context";
|
|
4
6
|
|
|
5
7
|
function aspectToRatio(aspect: string): number {
|
|
6
8
|
const [w, h] = aspect.split(":").map(Number);
|
|
@@ -16,11 +18,16 @@ export function SnapImage({
|
|
|
16
18
|
const url = String(props.url ?? "");
|
|
17
19
|
const alt = String(props.alt ?? "");
|
|
18
20
|
const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
|
|
21
|
+
const stackDir = useSnapStackDirection();
|
|
22
|
+
const inHorizontalStack = stackDir === "horizontal";
|
|
19
23
|
|
|
20
24
|
return (
|
|
21
25
|
<AspectRatio
|
|
22
26
|
ratio={ratio}
|
|
23
|
-
className=
|
|
27
|
+
className={cn(
|
|
28
|
+
"relative overflow-hidden rounded-lg",
|
|
29
|
+
inHorizontalStack ? "min-w-0 flex-1 basis-0" : "w-full",
|
|
30
|
+
)}
|
|
24
31
|
>
|
|
25
32
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
26
33
|
<img
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
|
+
import {
|
|
6
|
+
countRenderableChildren,
|
|
7
|
+
horizontalChildrenAreAllButtons,
|
|
8
|
+
} from "../../stack-horizontal-utils.js";
|
|
9
|
+
import {
|
|
10
|
+
SnapStackDirectionProvider,
|
|
11
|
+
useSnapStackDirection,
|
|
12
|
+
} from "../stack-direction-context";
|
|
5
13
|
|
|
6
14
|
const VGAP: Record<string, string> = {
|
|
7
15
|
none: "gap-0",
|
|
@@ -17,7 +25,7 @@ const HGAP: Record<string, string> = {
|
|
|
17
25
|
lg: "gap-3",
|
|
18
26
|
};
|
|
19
27
|
|
|
20
|
-
const
|
|
28
|
+
const JUSTIFY_FLEX: Record<string, string> = {
|
|
21
29
|
start: "justify-start",
|
|
22
30
|
center: "justify-center",
|
|
23
31
|
end: "justify-end",
|
|
@@ -25,6 +33,16 @@ const JUSTIFY: Record<string, string> = {
|
|
|
25
33
|
around: "justify-around",
|
|
26
34
|
};
|
|
27
35
|
|
|
36
|
+
/** Equal columns for explicit `columns` prop and for all-button horizontal rows. */
|
|
37
|
+
const COLUMN_GRID_CLASS: Record<number, string> = {
|
|
38
|
+
1: "grid grid-cols-1 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
39
|
+
2: "grid grid-cols-2 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
40
|
+
3: "grid grid-cols-3 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
41
|
+
4: "grid grid-cols-4 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
42
|
+
5: "grid grid-cols-5 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
43
|
+
6: "grid grid-cols-6 auto-rows-auto items-stretch [&>*]:min-w-0",
|
|
44
|
+
};
|
|
45
|
+
|
|
28
46
|
export function SnapStack({
|
|
29
47
|
element: { props },
|
|
30
48
|
children,
|
|
@@ -32,24 +50,79 @@ export function SnapStack({
|
|
|
32
50
|
element: { props: Record<string, unknown> };
|
|
33
51
|
children?: ReactNode;
|
|
34
52
|
}) {
|
|
53
|
+
const parentDirection = useSnapStackDirection();
|
|
35
54
|
const direction = String(props.direction ?? "vertical");
|
|
36
55
|
const gapKey = String(props.gap ?? "md");
|
|
37
56
|
const isHorizontal = direction === "horizontal";
|
|
38
57
|
const gap = isHorizontal
|
|
39
58
|
? (HGAP[gapKey] ?? "gap-2")
|
|
40
59
|
: (VGAP[gapKey] ?? "gap-4");
|
|
41
|
-
const
|
|
60
|
+
const justifyKey = props.justify ? String(props.justify) : undefined;
|
|
61
|
+
const justifyFlex = justifyKey ? JUSTIFY_FLEX[justifyKey] : undefined;
|
|
62
|
+
const buttonRowGrid =
|
|
63
|
+
isHorizontal && horizontalChildrenAreAllButtons(children);
|
|
64
|
+
const buttonRowCount = buttonRowGrid
|
|
65
|
+
? countRenderableChildren(children)
|
|
66
|
+
: 0;
|
|
67
|
+
|
|
68
|
+
const columnsRaw = props.columns;
|
|
69
|
+
const columns =
|
|
70
|
+
typeof columnsRaw === "number" &&
|
|
71
|
+
columnsRaw >= 2 &&
|
|
72
|
+
columnsRaw <= 6 &&
|
|
73
|
+
Number.isInteger(columnsRaw)
|
|
74
|
+
? columnsRaw
|
|
75
|
+
: undefined;
|
|
76
|
+
const explicitColumnGrid =
|
|
77
|
+
isHorizontal && columns !== undefined && !buttonRowGrid;
|
|
78
|
+
const columnGridClass =
|
|
79
|
+
explicitColumnGrid && columns !== undefined
|
|
80
|
+
? COLUMN_GRID_CLASS[columns]
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Row peers under a horizontal stack must shrink and share width (`flex-1` + `min-w-0`).
|
|
85
|
+
* Avoid `w-full` here: it resolves to 100% of the flex/grid container and fights peer sizing,
|
|
86
|
+
* so each column stacks on its own wrapped row instead of sitting side-by-side.
|
|
87
|
+
*/
|
|
88
|
+
const isRowChild = parentDirection === "horizontal";
|
|
89
|
+
const rootWidthClass = isRowChild
|
|
90
|
+
? "min-w-0 flex-1 basis-0 max-w-full"
|
|
91
|
+
: "w-full min-w-0";
|
|
92
|
+
|
|
93
|
+
const justifyBlockGrid =
|
|
94
|
+
justifyFlex &&
|
|
95
|
+
(!isHorizontal || (!buttonRowGrid && !explicitColumnGrid));
|
|
96
|
+
|
|
97
|
+
/** Single flex row (nowrap): peers stay side-by-side and shrink via min-w-0 / flex-1 on nested stacks. */
|
|
98
|
+
const horizontalFlexClasses =
|
|
99
|
+
"flex min-w-0 flex-row flex-nowrap items-stretch [&>*]:min-w-0";
|
|
42
100
|
|
|
43
101
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
"flex w-full",
|
|
47
|
-
isHorizontal ? "flex-row items-center flex-wrap" : "flex-col",
|
|
48
|
-
gap,
|
|
49
|
-
justify,
|
|
50
|
-
)}
|
|
102
|
+
<SnapStackDirectionProvider
|
|
103
|
+
direction={isHorizontal ? "horizontal" : "vertical"}
|
|
51
104
|
>
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
<div
|
|
106
|
+
className={cn(
|
|
107
|
+
rootWidthClass,
|
|
108
|
+
isHorizontal
|
|
109
|
+
? buttonRowGrid &&
|
|
110
|
+
buttonRowCount >= 1 &&
|
|
111
|
+
buttonRowCount <= 6 &&
|
|
112
|
+
COLUMN_GRID_CLASS[buttonRowCount]
|
|
113
|
+
? cn(
|
|
114
|
+
COLUMN_GRID_CLASS[buttonRowCount]!,
|
|
115
|
+
gap,
|
|
116
|
+
"[&>*]:w-full",
|
|
117
|
+
)
|
|
118
|
+
: explicitColumnGrid && columnGridClass
|
|
119
|
+
? cn(columnGridClass, gap)
|
|
120
|
+
: cn(horizontalFlexClasses, gap, justifyBlockGrid ? justifyFlex : undefined)
|
|
121
|
+
: cn("flex min-w-0 w-full flex-col", gap, justifyFlex),
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{children}
|
|
125
|
+
</div>
|
|
126
|
+
</SnapStackDirectionProvider>
|
|
54
127
|
);
|
|
55
128
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Text } from "@neynar/ui/typography";
|
|
4
|
+
import { cn } from "@neynar/ui/utils";
|
|
4
5
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
6
|
+
import { useSnapStackDirection } from "../stack-direction-context";
|
|
5
7
|
|
|
6
8
|
const SIZE_MAP = {
|
|
7
9
|
md: { textSize: "base" as const },
|
|
@@ -19,13 +21,18 @@ export function SnapText({
|
|
|
19
21
|
const align = (props.align as "left" | "center" | "right") ?? undefined;
|
|
20
22
|
const config = SIZE_MAP[size] ?? SIZE_MAP.md;
|
|
21
23
|
const colors = useSnapColors();
|
|
24
|
+
const stackDir = useSnapStackDirection();
|
|
25
|
+
const inHorizontalStack = stackDir === "horizontal";
|
|
22
26
|
|
|
23
27
|
return (
|
|
24
28
|
<Text
|
|
25
29
|
size={config.textSize}
|
|
26
30
|
weight={weight}
|
|
27
31
|
align={align}
|
|
28
|
-
className=
|
|
32
|
+
className={cn(
|
|
33
|
+
/** Row peers hug content like RN `wrapRow`; avoid `flex-1` stretching peers across the row. */
|
|
34
|
+
inHorizontalStack ? "min-w-0 shrink" : "flex-1",
|
|
35
|
+
)}
|
|
29
36
|
style={{ color: colors.text }}
|
|
30
37
|
>
|
|
31
38
|
{content}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export type SnapStackDirection = "vertical" | "horizontal";
|
|
6
|
+
|
|
7
|
+
const SnapStackDirectionContext = createContext<SnapStackDirection | undefined>(
|
|
8
|
+
undefined,
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export function SnapStackDirectionProvider({
|
|
12
|
+
direction,
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
direction: SnapStackDirection;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<SnapStackDirectionContext.Provider value={direction}>
|
|
20
|
+
{children}
|
|
21
|
+
</SnapStackDirectionContext.Provider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useSnapStackDirection(): SnapStackDirection | undefined {
|
|
26
|
+
return useContext(SnapStackDirectionContext);
|
|
27
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { SnapPage, SnapActionHandlers } from "../index";
|
|
|
9
9
|
|
|
10
10
|
const SNAP_MAX_HEIGHT = 500;
|
|
11
11
|
const SNAP_WARNING_HEIGHT = 700;
|
|
12
|
+
const SHOW_MORE_OVERHANG = 14;
|
|
12
13
|
|
|
13
14
|
// ─── Default validation error fallback ────────────────
|
|
14
15
|
|
|
@@ -166,7 +167,12 @@ export function SnapCardV2({
|
|
|
166
167
|
const containerMaxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : undefined;
|
|
167
168
|
|
|
168
169
|
return (
|
|
169
|
-
|
|
170
|
+
<div
|
|
171
|
+
style={{
|
|
172
|
+
paddingBottom:
|
|
173
|
+
!showOverflowWarning && isExpandable ? SHOW_MORE_OVERHANG : 0,
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
170
176
|
<div
|
|
171
177
|
style={{
|
|
172
178
|
position: "relative",
|
|
@@ -307,6 +313,6 @@ export function SnapCardV2({
|
|
|
307
313
|
{actionError}
|
|
308
314
|
</div>
|
|
309
315
|
)}
|
|
310
|
-
|
|
316
|
+
</div>
|
|
311
317
|
);
|
|
312
318
|
}
|
|
@@ -66,13 +66,17 @@ export function SnapCellGrid({
|
|
|
66
66
|
const ringOuter = appearance === "dark" ? "#fff" : "#000";
|
|
67
67
|
const ringInner = appearance === "dark" ? "#000" : "#fff";
|
|
68
68
|
|
|
69
|
+
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
70
|
+
const emptyCellBg =
|
|
71
|
+
appearance === "dark" ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.05)";
|
|
72
|
+
|
|
69
73
|
const rowEls = [];
|
|
70
74
|
for (let r = 0; r < rows; r++) {
|
|
71
75
|
const rowCells = [];
|
|
72
76
|
for (let c = 0; c < cols; c++) {
|
|
73
77
|
const cell = cellMap.get(`${r},${c}`);
|
|
74
78
|
const selected = interactive && isSelected(r, c);
|
|
75
|
-
const bg = cell?.color ? hex(cell.color) :
|
|
79
|
+
const bg = cell?.color ? hex(cell.color) : emptyCellBg;
|
|
76
80
|
|
|
77
81
|
const cellContent = cell?.content ? (
|
|
78
82
|
<Text style={[styles.cellText, { color: colors.textPrimary }]}>
|
|
@@ -121,18 +125,9 @@ export function SnapCellGrid({
|
|
|
121
125
|
);
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
125
|
-
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
126
|
-
: null;
|
|
127
|
-
|
|
128
128
|
return (
|
|
129
129
|
<View style={[styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }]}>
|
|
130
130
|
{rowEls}
|
|
131
|
-
{selectionLabel ? (
|
|
132
|
-
<Text style={[styles.selectionText, { color: colors.textSecondary }]}>
|
|
133
|
-
{selectionLabel}
|
|
134
|
-
</Text>
|
|
135
|
-
) : null}
|
|
136
131
|
</View>
|
|
137
132
|
);
|
|
138
133
|
}
|
|
@@ -153,5 +148,4 @@ const styles = StyleSheet.create({
|
|
|
153
148
|
justifyContent: "center",
|
|
154
149
|
},
|
|
155
150
|
cellText: { fontSize: 12, lineHeight: 16, fontWeight: "600" },
|
|
156
|
-
selectionText: { fontSize: 11, fontFamily: "monospace", marginTop: 6 },
|
|
157
151
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
2
|
import { Image } from "expo-image";
|
|
3
3
|
import { StyleSheet, View } from "react-native";
|
|
4
|
+
import { useSnapStackDirection } from "../stack-direction-context";
|
|
4
5
|
|
|
5
6
|
function aspectToRatio(aspect: string): number {
|
|
6
7
|
const [w, h] = aspect.split(":").map(Number);
|
|
@@ -14,9 +15,17 @@ export function SnapImage({
|
|
|
14
15
|
const url = String(props.url ?? "");
|
|
15
16
|
const alt = String(props.alt ?? "");
|
|
16
17
|
const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
|
|
18
|
+
const stackDir = useSnapStackDirection();
|
|
19
|
+
const inHorizontalStack = stackDir === "horizontal";
|
|
17
20
|
|
|
18
21
|
return (
|
|
19
|
-
<View
|
|
22
|
+
<View
|
|
23
|
+
style={[
|
|
24
|
+
styles.frame,
|
|
25
|
+
inHorizontalStack ? styles.frameInHorizontalRow : styles.frameFullWidth,
|
|
26
|
+
{ aspectRatio: ratio },
|
|
27
|
+
]}
|
|
28
|
+
>
|
|
20
29
|
<Image
|
|
21
30
|
source={{ uri: url }}
|
|
22
31
|
style={StyleSheet.absoluteFill}
|
|
@@ -29,9 +38,15 @@ export function SnapImage({
|
|
|
29
38
|
|
|
30
39
|
const styles = StyleSheet.create({
|
|
31
40
|
frame: {
|
|
32
|
-
width: "100%",
|
|
33
41
|
borderRadius: 8,
|
|
34
42
|
overflow: "hidden",
|
|
35
43
|
backgroundColor: "#f3f4f6",
|
|
36
44
|
},
|
|
45
|
+
frameFullWidth: {
|
|
46
|
+
width: "100%",
|
|
47
|
+
},
|
|
48
|
+
frameInHorizontalRow: {
|
|
49
|
+
flex: 1,
|
|
50
|
+
minWidth: 0,
|
|
51
|
+
},
|
|
37
52
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { StyleSheet, Text, View } from "react-native";
|
|
4
|
+
import { useSnapStackDirection } from "../stack-direction-context";
|
|
4
5
|
import { useSnapTheme } from "../theme";
|
|
5
6
|
|
|
6
7
|
export function SnapItem({
|
|
@@ -12,12 +13,19 @@ export function SnapItem({
|
|
|
12
13
|
const description = props.description
|
|
13
14
|
? String(props.description)
|
|
14
15
|
: undefined;
|
|
15
|
-
|
|
16
|
+
/** Match web `Item className="flex-1"`: row peers must share width or title/description collapse. */
|
|
17
|
+
const rowPeer = useSnapStackDirection() === "horizontal";
|
|
16
18
|
|
|
17
19
|
const containerVariant = { paddingVertical: 6, paddingHorizontal: 10 };
|
|
18
20
|
|
|
19
21
|
return (
|
|
20
|
-
<View
|
|
22
|
+
<View
|
|
23
|
+
style={[
|
|
24
|
+
styles.container,
|
|
25
|
+
containerVariant,
|
|
26
|
+
rowPeer && styles.rowPeer,
|
|
27
|
+
]}
|
|
28
|
+
>
|
|
21
29
|
<View style={styles.content}>
|
|
22
30
|
{title ? <Text style={[styles.title, { color: colors.text }]}>{title}</Text> : null}
|
|
23
31
|
{description ? (
|
|
@@ -40,6 +48,10 @@ const styles = StyleSheet.create({
|
|
|
40
48
|
flexDirection: "row",
|
|
41
49
|
alignItems: "center",
|
|
42
50
|
},
|
|
51
|
+
rowPeer: {
|
|
52
|
+
flex: 1,
|
|
53
|
+
minWidth: 0,
|
|
54
|
+
},
|
|
43
55
|
content: {
|
|
44
56
|
flex: 1,
|
|
45
57
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
2
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { useSnapStackDirection } from "../stack-direction-context";
|
|
3
4
|
import { useSnapPalette } from "../use-snap-palette";
|
|
4
5
|
import { useSnapTheme } from "../theme";
|
|
5
6
|
|
|
@@ -12,9 +13,10 @@ export function SnapProgress({
|
|
|
12
13
|
const max = Math.max(1, Number(props.max ?? 100));
|
|
13
14
|
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
14
15
|
const label = props.label != null ? String(props.label) : null;
|
|
16
|
+
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
15
17
|
|
|
16
18
|
return (
|
|
17
|
-
<View style={styles.wrap}>
|
|
19
|
+
<View style={[styles.wrap, inHorizontalStack ? styles.wrapRowPeer : styles.wrapCol]}>
|
|
18
20
|
{label ? (
|
|
19
21
|
<Text style={[styles.label, { color: colors.textSecondary }]}>{label}</Text>
|
|
20
22
|
) : null}
|
|
@@ -26,7 +28,11 @@ export function SnapProgress({
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const styles = StyleSheet.create({
|
|
29
|
-
wrap: {
|
|
31
|
+
wrap: { gap: 4 },
|
|
32
|
+
/** Vertical stacks: span card width (matches web `w-full`). */
|
|
33
|
+
wrapCol: { width: "100%" },
|
|
34
|
+
/** Horizontal row peers: share space; `width: 100%` each overflows the row. */
|
|
35
|
+
wrapRowPeer: { flex: 1, minWidth: 0 },
|
|
30
36
|
label: { fontSize: 13, lineHeight: 18 },
|
|
31
37
|
track: {
|
|
32
38
|
height: 10,
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
|
-
import type
|
|
2
|
+
import { Children, type ReactNode } from "react";
|
|
3
3
|
import { StyleSheet, View } from "react-native";
|
|
4
|
+
import {
|
|
5
|
+
countRenderableChildren,
|
|
6
|
+
horizontalChildrenAreAllButtons,
|
|
7
|
+
} from "../../stack-horizontal-utils.js";
|
|
8
|
+
import {
|
|
9
|
+
SnapStackDirectionProvider,
|
|
10
|
+
useSnapStackDirection,
|
|
11
|
+
} from "../stack-direction-context";
|
|
4
12
|
|
|
5
13
|
const VGAP: Record<string, number> = {
|
|
6
14
|
none: 0,
|
|
@@ -24,10 +32,23 @@ const JUSTIFY: Record<string, "flex-start" | "center" | "flex-end" | "space-betw
|
|
|
24
32
|
around: "space-around",
|
|
25
33
|
};
|
|
26
34
|
|
|
35
|
+
/** Equal-width cells for explicit `columns` and all-button horizontal rows. */
|
|
36
|
+
function wrapEqualColumnCells(children: ReactNode): ReactNode {
|
|
37
|
+
const cells = Children.toArray(children).filter(
|
|
38
|
+
(c) => c != null && c !== false,
|
|
39
|
+
);
|
|
40
|
+
return cells.map((child, i) => (
|
|
41
|
+
<View key={i} style={styles.equalColumnCell}>
|
|
42
|
+
{child}
|
|
43
|
+
</View>
|
|
44
|
+
));
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
export function SnapStack({
|
|
28
48
|
element: { props },
|
|
29
49
|
children,
|
|
30
50
|
}: ComponentRenderProps<Record<string, unknown>> & { children?: ReactNode }) {
|
|
51
|
+
const parentDirection = useSnapStackDirection();
|
|
31
52
|
const direction = String(props.direction ?? "vertical");
|
|
32
53
|
const rawGap = props.gap;
|
|
33
54
|
const isHorizontal = direction === "horizontal";
|
|
@@ -38,29 +59,110 @@ export function SnapStack({
|
|
|
38
59
|
: typeof rawGap === "string" && rawGap in gapMap
|
|
39
60
|
? gapMap[rawGap]!
|
|
40
61
|
: isHorizontal ? HGAP.md! : VGAP.md!;
|
|
41
|
-
const
|
|
62
|
+
const buttonRowGrid =
|
|
63
|
+
isHorizontal && horizontalChildrenAreAllButtons(children);
|
|
64
|
+
const buttonRowCount = buttonRowGrid
|
|
65
|
+
? countRenderableChildren(children)
|
|
66
|
+
: 0;
|
|
67
|
+
|
|
68
|
+
const columnsRaw = props.columns;
|
|
69
|
+
const columns =
|
|
70
|
+
typeof columnsRaw === "number" &&
|
|
71
|
+
columnsRaw >= 2 &&
|
|
72
|
+
columnsRaw <= 6 &&
|
|
73
|
+
Number.isInteger(columnsRaw)
|
|
74
|
+
? columnsRaw
|
|
75
|
+
: undefined;
|
|
76
|
+
const explicitColumnGrid =
|
|
77
|
+
isHorizontal && columns !== undefined && !buttonRowGrid;
|
|
78
|
+
|
|
79
|
+
const justify =
|
|
80
|
+
props.justify &&
|
|
81
|
+
(!isHorizontal || (!buttonRowGrid && !explicitColumnGrid))
|
|
82
|
+
? JUSTIFY[String(props.justify)]
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
const isRowChild = parentDirection === "horizontal";
|
|
86
|
+
|
|
87
|
+
const packedHorizontal =
|
|
88
|
+
isHorizontal &&
|
|
89
|
+
((buttonRowGrid &&
|
|
90
|
+
buttonRowCount >= 1 &&
|
|
91
|
+
buttonRowCount <= 6) ||
|
|
92
|
+
explicitColumnGrid);
|
|
93
|
+
|
|
94
|
+
let horizontalBody: ReactNode = children;
|
|
95
|
+
if (
|
|
96
|
+
isHorizontal &&
|
|
97
|
+
buttonRowGrid &&
|
|
98
|
+
buttonRowCount >= 1 &&
|
|
99
|
+
buttonRowCount <= 6
|
|
100
|
+
) {
|
|
101
|
+
horizontalBody = wrapEqualColumnCells(children);
|
|
102
|
+
} else if (isHorizontal && explicitColumnGrid && columns !== undefined) {
|
|
103
|
+
horizontalBody = wrapEqualColumnCells(children);
|
|
104
|
+
}
|
|
42
105
|
|
|
43
106
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
styles.stack,
|
|
47
|
-
isHorizontal ? styles.horizontal : undefined,
|
|
48
|
-
{ gap },
|
|
49
|
-
justify ? { justifyContent: justify } : undefined,
|
|
50
|
-
]}
|
|
107
|
+
<SnapStackDirectionProvider
|
|
108
|
+
direction={isHorizontal ? "horizontal" : "vertical"}
|
|
51
109
|
>
|
|
52
|
-
|
|
53
|
-
|
|
110
|
+
<View
|
|
111
|
+
style={[
|
|
112
|
+
isRowChild ? styles.stackRowChild : styles.stack,
|
|
113
|
+
isHorizontal
|
|
114
|
+
? packedHorizontal
|
|
115
|
+
? styles.horizontalPacked
|
|
116
|
+
: styles.horizontalDefault
|
|
117
|
+
: styles.verticalStack,
|
|
118
|
+
{ gap },
|
|
119
|
+
justify ? { justifyContent: justify } : undefined,
|
|
120
|
+
]}
|
|
121
|
+
>
|
|
122
|
+
{horizontalBody}
|
|
123
|
+
</View>
|
|
124
|
+
</SnapStackDirectionProvider>
|
|
54
125
|
);
|
|
55
126
|
}
|
|
56
127
|
|
|
57
128
|
const styles = StyleSheet.create({
|
|
58
129
|
stack: {
|
|
59
130
|
width: "100%",
|
|
131
|
+
minWidth: 0,
|
|
132
|
+
},
|
|
133
|
+
verticalStack: {
|
|
134
|
+
width: "100%",
|
|
135
|
+
minWidth: 0,
|
|
60
136
|
},
|
|
61
|
-
horizontal
|
|
137
|
+
/** Nested stack inside a horizontal row — share width with siblings (matches web flex peers). */
|
|
138
|
+
stackRowChild: {
|
|
139
|
+
flexGrow: 1,
|
|
140
|
+
flexShrink: 1,
|
|
141
|
+
flexBasis: 0,
|
|
142
|
+
minWidth: 0,
|
|
143
|
+
maxWidth: "100%",
|
|
144
|
+
alignSelf: "stretch",
|
|
145
|
+
},
|
|
146
|
+
/** Default horizontal row: single line, equal-height peers. */
|
|
147
|
+
horizontalDefault: {
|
|
62
148
|
flexDirection: "row",
|
|
63
|
-
alignItems: "
|
|
64
|
-
flexWrap: "
|
|
149
|
+
alignItems: "stretch",
|
|
150
|
+
flexWrap: "nowrap",
|
|
151
|
+
width: "100%",
|
|
152
|
+
minWidth: 0,
|
|
153
|
+
},
|
|
154
|
+
/** Single row for packed equal-width cells (button grids & explicit columns). */
|
|
155
|
+
horizontalPacked: {
|
|
156
|
+
flexDirection: "row",
|
|
157
|
+
flexWrap: "nowrap",
|
|
158
|
+
alignItems: "stretch",
|
|
159
|
+
width: "100%",
|
|
160
|
+
minWidth: 0,
|
|
161
|
+
},
|
|
162
|
+
equalColumnCell: {
|
|
163
|
+
flexGrow: 1,
|
|
164
|
+
flexShrink: 1,
|
|
165
|
+
flexBasis: 0,
|
|
166
|
+
minWidth: 0,
|
|
65
167
|
},
|
|
66
168
|
});
|