@farcaster/snap 2.5.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/react/catalog-renderer.js +2 -0
- package/dist/react/components/action-button.js +9 -1
- package/dist/react/components/cell-grid.js +16 -2
- package/dist/react/components/image.js +5 -2
- package/dist/react/components/paginator.d.ts +7 -0
- package/dist/react/components/paginator.js +103 -0
- package/dist/react/components/stack.js +21 -18
- package/dist/react/components/text.js +19 -1
- package/dist/react/snap-version-context.d.ts +3 -0
- package/dist/react/snap-version-context.js +7 -0
- package/dist/react/snap-view-core.d.ts +1 -1
- package/dist/react/snap-view-core.js +27 -4
- package/dist/react-native/catalog-renderer.js +2 -0
- package/dist/react-native/components/snap-action-button.js +8 -2
- package/dist/react-native/components/snap-cell-grid.js +16 -2
- package/dist/react-native/components/snap-image.js +29 -4
- package/dist/react-native/components/snap-paginator.d.ts +5 -0
- package/dist/react-native/components/snap-paginator.js +194 -0
- package/dist/react-native/components/snap-text.js +10 -3
- package/dist/react-native/expand-state.d.ts +19 -0
- package/dist/react-native/expand-state.js +18 -0
- package/dist/react-native/index.d.ts +7 -1
- package/dist/react-native/index.js +3 -3
- package/dist/react-native/snap-version-context.d.ts +3 -0
- package/dist/react-native/snap-version-context.js +6 -0
- package/dist/react-native/snap-view-core.d.ts +1 -1
- package/dist/react-native/snap-view-core.js +27 -4
- package/dist/react-native/v1/snap-view.d.ts +7 -1
- package/dist/react-native/v1/snap-view.js +35 -11
- package/dist/react-native/v2/snap-view.d.ts +7 -1
- package/dist/react-native/v2/snap-view.js +60 -17
- package/dist/ui/catalog.d.ts +45 -0
- package/dist/ui/catalog.js +20 -3
- package/dist/ui/cell-grid.d.ts +5 -0
- package/dist/ui/cell-grid.js +2 -1
- package/dist/ui/image.d.ts +4 -1
- package/dist/ui/image.js +3 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/paginator-state.d.ts +18 -0
- package/dist/ui/paginator-state.js +47 -0
- package/dist/ui/paginator.d.ts +17 -0
- package/dist/ui/paginator.js +8 -0
- package/dist/ui/text.d.ts +1 -0
- package/dist/ui/text.js +1 -0
- package/dist/validator.js +16 -3
- package/llms.txt +19 -4
- package/package.json +1 -1
- package/src/constants.ts +1 -0
- package/src/react/catalog-renderer.tsx +2 -0
- package/src/react/components/action-button.tsx +13 -2
- package/src/react/components/cell-grid.tsx +22 -2
- package/src/react/components/image.tsx +17 -0
- package/src/react/components/paginator.tsx +208 -0
- package/src/react/components/stack.tsx +20 -18
- package/src/react/components/text.tsx +20 -1
- package/src/react/snap-version-context.tsx +12 -0
- package/src/react/snap-view-core.tsx +44 -12
- package/src/react-native/catalog-renderer.tsx +2 -0
- package/src/react-native/components/snap-action-button.tsx +10 -2
- package/src/react-native/components/snap-cell-grid.tsx +22 -2
- package/src/react-native/components/snap-image.tsx +40 -1
- package/src/react-native/components/snap-paginator.tsx +283 -0
- package/src/react-native/components/snap-text.tsx +11 -2
- package/src/react-native/expand-state.ts +48 -0
- package/src/react-native/index.tsx +15 -0
- package/src/react-native/snap-version-context.tsx +10 -0
- package/src/react-native/snap-view-core.tsx +47 -12
- package/src/react-native/v1/snap-view.tsx +57 -10
- package/src/react-native/v2/snap-view.tsx +88 -17
- package/src/ui/catalog.ts +25 -3
- package/src/ui/cell-grid.ts +2 -0
- package/src/ui/image.ts +3 -1
- package/src/ui/index.ts +3 -0
- package/src/ui/paginator-state.ts +67 -0
- package/src/ui/paginator.ts +11 -0
- package/src/ui/text.ts +1 -0
- package/src/validator.ts +19 -3
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { useStateStore } from "@json-render/react";
|
|
4
5
|
import { ExternalLink } from "lucide-react";
|
|
5
6
|
import { Button } from "@neynar/ui/button";
|
|
6
7
|
import { cn } from "@neynar/ui/utils";
|
|
7
8
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
9
|
+
import {
|
|
10
|
+
getPaginatorAction,
|
|
11
|
+
runPaginatorAction,
|
|
12
|
+
} from "../../ui/paginator-state";
|
|
8
13
|
import { useSnapStackDirection } from "../stack-direction-context";
|
|
9
14
|
import { ICON_MAP } from "./icon";
|
|
10
15
|
|
|
@@ -36,6 +41,8 @@ export function SnapActionButton({
|
|
|
36
41
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
37
42
|
const colors = useSnapColors();
|
|
38
43
|
const [hovered, setHovered] = useState(false);
|
|
44
|
+
const stateStore = useStateStore();
|
|
45
|
+
const paginatorAction = getPaginatorAction(element.on);
|
|
39
46
|
|
|
40
47
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
41
48
|
const showExternalIcon = isExternalLinkAction(element.on);
|
|
@@ -75,9 +82,13 @@ export function SnapActionButton({
|
|
|
75
82
|
<Button
|
|
76
83
|
type="button"
|
|
77
84
|
variant={isPrimary ? "default" : "secondary"}
|
|
78
|
-
className={cn("w-full gap-2")}
|
|
85
|
+
className={cn("h-8 w-full gap-2 px-3 text-sm")}
|
|
79
86
|
style={style}
|
|
80
|
-
onClick={() =>
|
|
87
|
+
onClick={() => {
|
|
88
|
+
if (!runPaginatorAction(stateStore, paginatorAction)) {
|
|
89
|
+
emit("press");
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
81
92
|
onPointerEnter={() => setHovered(true)}
|
|
82
93
|
onPointerLeave={() => setHovered(false)}
|
|
83
94
|
>
|
|
@@ -5,6 +5,10 @@ import { useStateStore } from "@json-render/react";
|
|
|
5
5
|
import { cn } from "@neynar/ui/utils";
|
|
6
6
|
import { POST_GRID_TAP_KEY, readableTextOnHex } from "@farcaster/snap";
|
|
7
7
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
8
|
+
import {
|
|
9
|
+
getPaginatorAction,
|
|
10
|
+
runPaginatorAction,
|
|
11
|
+
} from "../../ui/paginator-state";
|
|
8
12
|
|
|
9
13
|
export function SnapCellGrid({
|
|
10
14
|
element: { props, on },
|
|
@@ -13,8 +17,10 @@ export function SnapCellGrid({
|
|
|
13
17
|
element: { props: Record<string, unknown>; on?: Record<string, unknown> };
|
|
14
18
|
emit: (name: string) => void;
|
|
15
19
|
}) {
|
|
16
|
-
const
|
|
20
|
+
const stateStore = useStateStore();
|
|
21
|
+
const { get, set } = stateStore;
|
|
17
22
|
const colors = useSnapColors();
|
|
23
|
+
const paginatorAction = getPaginatorAction(on);
|
|
18
24
|
const cols = Number(props.cols ?? 2);
|
|
19
25
|
const rows = Number(props.rows ?? 2);
|
|
20
26
|
const select = String(props.select ?? "off");
|
|
@@ -28,6 +34,13 @@ export function SnapCellGrid({
|
|
|
28
34
|
const gapPx = gapMap[gap] ?? 1;
|
|
29
35
|
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
30
36
|
const squareCells = props.cellAspectRatio === "square";
|
|
37
|
+
const maxWidthKey = typeof props.maxWidth === "string" ? props.maxWidth : undefined;
|
|
38
|
+
const maxWidthMap: Record<string, number | undefined> = {
|
|
39
|
+
sm: 160,
|
|
40
|
+
md: 220,
|
|
41
|
+
lg: undefined,
|
|
42
|
+
};
|
|
43
|
+
const maxWidth = maxWidthKey ? maxWidthMap[maxWidthKey] : undefined;
|
|
31
44
|
|
|
32
45
|
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
33
46
|
const tapPath = `/inputs/${name}`;
|
|
@@ -75,7 +88,12 @@ export function SnapCellGrid({
|
|
|
75
88
|
} else {
|
|
76
89
|
set(tapPath, wire);
|
|
77
90
|
}
|
|
78
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
hasPressAction &&
|
|
93
|
+
!runPaginatorAction(stateStore, paginatorAction)
|
|
94
|
+
) {
|
|
95
|
+
emit("press");
|
|
96
|
+
}
|
|
79
97
|
};
|
|
80
98
|
|
|
81
99
|
/** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
|
|
@@ -138,6 +156,8 @@ export function SnapCellGrid({
|
|
|
138
156
|
style={{
|
|
139
157
|
display: "grid",
|
|
140
158
|
width: "100%",
|
|
159
|
+
maxWidth,
|
|
160
|
+
marginInline: maxWidth ? "auto" : undefined,
|
|
141
161
|
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
142
162
|
gap: gapPx,
|
|
143
163
|
padding: 4,
|
|
@@ -17,6 +17,9 @@ export function SnapImage({
|
|
|
17
17
|
}) {
|
|
18
18
|
const url = String(props.url ?? "");
|
|
19
19
|
const alt = String(props.alt ?? "");
|
|
20
|
+
const title = props.title ? String(props.title) : "";
|
|
21
|
+
const subtitle = props.subtitle ? String(props.subtitle) : "";
|
|
22
|
+
const hasOverlay = title.length > 0 || subtitle.length > 0;
|
|
20
23
|
const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
|
|
21
24
|
const stackDir = useSnapStackDirection();
|
|
22
25
|
const inHorizontalStack = stackDir === "horizontal";
|
|
@@ -35,6 +38,20 @@ export function SnapImage({
|
|
|
35
38
|
alt={alt}
|
|
36
39
|
className="absolute inset-0 size-full object-cover"
|
|
37
40
|
/>
|
|
41
|
+
{hasOverlay && (
|
|
42
|
+
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 via-black/35 to-transparent p-3 pt-8 text-white">
|
|
43
|
+
{title && (
|
|
44
|
+
<div className="truncate text-sm font-semibold leading-5">
|
|
45
|
+
{title}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
{subtitle && (
|
|
49
|
+
<div className="truncate text-xs font-medium leading-4 text-white/85">
|
|
50
|
+
{subtitle}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
38
55
|
</AspectRatio>
|
|
39
56
|
);
|
|
40
57
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
type CSSProperties,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { useStateStore } from "@json-render/react";
|
|
12
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
13
|
+
import { cn } from "@neynar/ui/utils";
|
|
14
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
15
|
+
import {
|
|
16
|
+
clampPaginatorPage,
|
|
17
|
+
pageFromValue,
|
|
18
|
+
SNAP_PAGINATOR_PAGE_COUNT_PATH,
|
|
19
|
+
SNAP_PAGINATOR_PAGE_PATH,
|
|
20
|
+
} from "../../ui/paginator-state";
|
|
21
|
+
|
|
22
|
+
function clampInitialPage(value: unknown, pageCount: number): number {
|
|
23
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return 0;
|
|
24
|
+
return Math.min(Math.max(value, 0), Math.max(pageCount - 1, 0));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SnapPaginator({
|
|
28
|
+
element: { props },
|
|
29
|
+
children,
|
|
30
|
+
}: {
|
|
31
|
+
element: { props: Record<string, unknown> };
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}) {
|
|
34
|
+
const pages = useMemo(
|
|
35
|
+
() => Children.toArray(children),
|
|
36
|
+
[children],
|
|
37
|
+
);
|
|
38
|
+
const colors = useSnapColors();
|
|
39
|
+
const { get, set } = useStateStore();
|
|
40
|
+
const initialPage = clampInitialPage(props.initialPage, pages.length);
|
|
41
|
+
const page = clampPaginatorPage(
|
|
42
|
+
pageFromValue(get(SNAP_PAGINATOR_PAGE_PATH), initialPage),
|
|
43
|
+
pages.length,
|
|
44
|
+
);
|
|
45
|
+
const activePage = Math.min(page, Math.max(pages.length - 1, 0));
|
|
46
|
+
const showControls = props.showControls !== false && pages.length > 1;
|
|
47
|
+
const showIndicators = props.showIndicators !== false && pages.length > 1;
|
|
48
|
+
const controlsPosition = props.controlsPosition === "top" ? "top" : "bottom";
|
|
49
|
+
const transition =
|
|
50
|
+
props.transition === "fade" ||
|
|
51
|
+
props.transition === "scale" ||
|
|
52
|
+
props.transition === "none"
|
|
53
|
+
? props.transition
|
|
54
|
+
: "slide";
|
|
55
|
+
const showControlBar = showControls || showIndicators;
|
|
56
|
+
const [transitionDirection, setTransitionDirection] =
|
|
57
|
+
useState<"next" | "previous">("next");
|
|
58
|
+
|
|
59
|
+
const canGoPrev = activePage > 0;
|
|
60
|
+
const canGoNext = activePage < pages.length - 1;
|
|
61
|
+
const goToPage = (targetPage: number) => {
|
|
62
|
+
const nextPage = clampPaginatorPage(targetPage, pages.length);
|
|
63
|
+
if (nextPage !== activePage) {
|
|
64
|
+
setTransitionDirection(nextPage > activePage ? "next" : "previous");
|
|
65
|
+
}
|
|
66
|
+
set(SNAP_PAGINATOR_PAGE_PATH, nextPage);
|
|
67
|
+
};
|
|
68
|
+
const goPrev = () => goToPage(activePage - 1);
|
|
69
|
+
const goNext = () => goToPage(activePage + 1);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (pages.length === 0) return;
|
|
73
|
+
const nextPage = clampPaginatorPage(
|
|
74
|
+
pageFromValue(get(SNAP_PAGINATOR_PAGE_PATH), initialPage),
|
|
75
|
+
pages.length,
|
|
76
|
+
);
|
|
77
|
+
if (get(SNAP_PAGINATOR_PAGE_PATH) !== nextPage) {
|
|
78
|
+
set(SNAP_PAGINATOR_PAGE_PATH, nextPage);
|
|
79
|
+
}
|
|
80
|
+
if (get(SNAP_PAGINATOR_PAGE_COUNT_PATH) !== pages.length) {
|
|
81
|
+
set(SNAP_PAGINATOR_PAGE_COUNT_PATH, pages.length);
|
|
82
|
+
}
|
|
83
|
+
}, [get, initialPage, pages.length, set]);
|
|
84
|
+
|
|
85
|
+
if (pages.length === 0) return null;
|
|
86
|
+
|
|
87
|
+
const pageAnimation =
|
|
88
|
+
transition === "none"
|
|
89
|
+
? undefined
|
|
90
|
+
: transition === "fade"
|
|
91
|
+
? "snapPaginatorFade 180ms ease-out"
|
|
92
|
+
: transition === "scale"
|
|
93
|
+
? "snapPaginatorScale 240ms cubic-bezier(0.16, 1, 0.3, 1)"
|
|
94
|
+
: "snapPaginatorSlide 260ms cubic-bezier(0.16, 1, 0.3, 1)";
|
|
95
|
+
|
|
96
|
+
const controlBar = showControlBar ? (
|
|
97
|
+
<div className="flex min-h-7 w-full items-center justify-between gap-2">
|
|
98
|
+
{showControls ? (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label="Previous page"
|
|
102
|
+
disabled={!canGoPrev}
|
|
103
|
+
onClick={goPrev}
|
|
104
|
+
className={cn(
|
|
105
|
+
"inline-flex size-7 items-center justify-center rounded-md border text-sm transition-opacity",
|
|
106
|
+
canGoPrev ? "cursor-pointer opacity-100" : "cursor-default opacity-35",
|
|
107
|
+
)}
|
|
108
|
+
style={{
|
|
109
|
+
borderColor: colors.border,
|
|
110
|
+
backgroundColor: colors.muted,
|
|
111
|
+
color: colors.text,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<ChevronLeft size={15} />
|
|
115
|
+
</button>
|
|
116
|
+
) : (
|
|
117
|
+
<span />
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{showIndicators ? (
|
|
121
|
+
<div className="flex flex-1 items-center justify-center gap-1.5">
|
|
122
|
+
{pages.map((_, index) => {
|
|
123
|
+
const current = index === activePage;
|
|
124
|
+
return (
|
|
125
|
+
<span
|
|
126
|
+
key={index}
|
|
127
|
+
aria-label={`Page ${index + 1}${current ? ", current" : ""}`}
|
|
128
|
+
className={cn(
|
|
129
|
+
"block rounded-full",
|
|
130
|
+
current ? "size-2.5" : "size-2",
|
|
131
|
+
)}
|
|
132
|
+
style={{
|
|
133
|
+
backgroundColor: current
|
|
134
|
+
? colors.accent
|
|
135
|
+
: colors.mode === "dark"
|
|
136
|
+
? "rgba(255,255,255,0.5)"
|
|
137
|
+
: "rgba(0,0,0,0.28)",
|
|
138
|
+
boxShadow: current
|
|
139
|
+
? `0 0 0 2px ${colors.mode === "dark" ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.12)"}`
|
|
140
|
+
: undefined,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<span className="flex-1" />
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{showControls ? (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label="Next page"
|
|
154
|
+
disabled={!canGoNext}
|
|
155
|
+
onClick={goNext}
|
|
156
|
+
className={cn(
|
|
157
|
+
"inline-flex size-7 items-center justify-center rounded-md border text-sm transition-opacity",
|
|
158
|
+
canGoNext ? "cursor-pointer opacity-100" : "cursor-default opacity-35",
|
|
159
|
+
)}
|
|
160
|
+
style={{
|
|
161
|
+
borderColor: colors.border,
|
|
162
|
+
backgroundColor: colors.muted,
|
|
163
|
+
color: colors.text,
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<ChevronRight size={15} />
|
|
167
|
+
</button>
|
|
168
|
+
) : (
|
|
169
|
+
<span />
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
) : null;
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="flex w-full min-w-0 flex-col gap-2">
|
|
176
|
+
{controlsPosition === "top" ? controlBar : null}
|
|
177
|
+
<div
|
|
178
|
+
key={activePage}
|
|
179
|
+
data-snap-paginator-page
|
|
180
|
+
className="w-full min-w-0"
|
|
181
|
+
style={{
|
|
182
|
+
"--snap-paginator-x": transitionDirection === "previous" ? "-22px" : "22px",
|
|
183
|
+
animation: pageAnimation,
|
|
184
|
+
} as CSSProperties}
|
|
185
|
+
>
|
|
186
|
+
{pages[activePage]}
|
|
187
|
+
</div>
|
|
188
|
+
{controlsPosition === "bottom" ? controlBar : null}
|
|
189
|
+
<style>{`
|
|
190
|
+
@keyframes snapPaginatorSlide {
|
|
191
|
+
from { opacity: 0.35; transform: translateX(var(--snap-paginator-x, 22px)) scale(0.985); }
|
|
192
|
+
to { opacity: 1; transform: translateX(0) scale(1); }
|
|
193
|
+
}
|
|
194
|
+
@keyframes snapPaginatorFade {
|
|
195
|
+
from { opacity: 0.2; }
|
|
196
|
+
to { opacity: 1; }
|
|
197
|
+
}
|
|
198
|
+
@keyframes snapPaginatorScale {
|
|
199
|
+
from { opacity: 0.25; transform: scale(0.94); }
|
|
200
|
+
to { opacity: 1; transform: scale(1); }
|
|
201
|
+
}
|
|
202
|
+
@media (prefers-reduced-motion: reduce) {
|
|
203
|
+
[data-snap-paginator-page] { animation: none !important; }
|
|
204
|
+
}
|
|
205
|
+
`}</style>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -13,18 +13,18 @@ import {
|
|
|
13
13
|
useSnapStackDirection,
|
|
14
14
|
} from "../stack-direction-context";
|
|
15
15
|
|
|
16
|
-
const VGAP: Record<string,
|
|
17
|
-
none:
|
|
18
|
-
sm:
|
|
19
|
-
md:
|
|
20
|
-
lg:
|
|
16
|
+
const VGAP: Record<string, number> = {
|
|
17
|
+
none: 0,
|
|
18
|
+
sm: 4,
|
|
19
|
+
md: 16,
|
|
20
|
+
lg: 24,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const HGAP: Record<string,
|
|
24
|
-
none:
|
|
25
|
-
sm:
|
|
26
|
-
md:
|
|
27
|
-
lg:
|
|
23
|
+
const HGAP: Record<string, number> = {
|
|
24
|
+
none: 0,
|
|
25
|
+
sm: 4,
|
|
26
|
+
md: 8,
|
|
27
|
+
lg: 16,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
const JUSTIFY_FLEX: Record<string, string> = {
|
|
@@ -100,7 +100,7 @@ export function SnapStack({
|
|
|
100
100
|
: isHorizontal
|
|
101
101
|
? defaultHorizontalGapSize(horizontalChildCount)
|
|
102
102
|
: "md";
|
|
103
|
-
const
|
|
103
|
+
const gapPx = isHorizontal
|
|
104
104
|
? (HGAP[gapKey] ?? HGAP.md!)
|
|
105
105
|
: (VGAP[gapKey] ?? VGAP.md!);
|
|
106
106
|
const columnGridClass =
|
|
@@ -125,13 +125,15 @@ export function SnapStack({
|
|
|
125
125
|
/** Single flex row (nowrap): peers stay side-by-side and shrink via min-w-0 / flex-1 on nested stacks. */
|
|
126
126
|
const horizontalFlexClasses =
|
|
127
127
|
"flex min-w-0 flex-row flex-nowrap items-stretch [&>*]:min-w-0";
|
|
128
|
-
const
|
|
129
|
-
|
|
128
|
+
const layoutStyle: CSSProperties = {
|
|
129
|
+
gap: gapPx,
|
|
130
|
+
...(explicitEqualWidth && equalWidthColumnCount !== undefined
|
|
130
131
|
? {
|
|
131
132
|
display: "grid",
|
|
132
133
|
gridTemplateColumns: `repeat(${equalWidthColumnCount}, minmax(0, 1fr))`,
|
|
133
134
|
}
|
|
134
|
-
:
|
|
135
|
+
: {}),
|
|
136
|
+
};
|
|
135
137
|
|
|
136
138
|
return (
|
|
137
139
|
<SnapStackDirectionProvider
|
|
@@ -142,11 +144,11 @@ export function SnapStack({
|
|
|
142
144
|
rootWidthClass,
|
|
143
145
|
isHorizontal
|
|
144
146
|
? explicitEqualWidth && columnGridClass
|
|
145
|
-
?
|
|
146
|
-
: cn(horizontalFlexClasses,
|
|
147
|
-
: cn("flex min-w-0 w-full flex-col",
|
|
147
|
+
? columnGridClass
|
|
148
|
+
: cn(horizontalFlexClasses, justifyBlockGrid ? justifyFlex : undefined)
|
|
149
|
+
: cn("flex min-w-0 w-full flex-col", justifyFlex),
|
|
148
150
|
)}
|
|
149
|
-
style={
|
|
151
|
+
style={layoutStyle}
|
|
150
152
|
>
|
|
151
153
|
{children}
|
|
152
154
|
</div>
|
|
@@ -4,6 +4,7 @@ import { Text } from "@neynar/ui/typography";
|
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
6
6
|
import { useSnapStackDirection } from "../stack-direction-context";
|
|
7
|
+
import { useSnapVersion } from "../snap-version-context";
|
|
7
8
|
|
|
8
9
|
const SIZE_MAP = {
|
|
9
10
|
md: { textSize: "base" as const },
|
|
@@ -22,7 +23,14 @@ export function SnapText({
|
|
|
22
23
|
const config = SIZE_MAP[size] ?? SIZE_MAP.md;
|
|
23
24
|
const colors = useSnapColors();
|
|
24
25
|
const stackDir = useSnapStackDirection();
|
|
26
|
+
const snapVersion = useSnapVersion();
|
|
25
27
|
const inHorizontalStack = stackDir === "horizontal";
|
|
28
|
+
const maxLines =
|
|
29
|
+
typeof props.maxLines === "number"
|
|
30
|
+
? props.maxLines
|
|
31
|
+
: snapVersion === "2.0"
|
|
32
|
+
? 1
|
|
33
|
+
: undefined;
|
|
26
34
|
|
|
27
35
|
return (
|
|
28
36
|
<Text
|
|
@@ -40,7 +48,18 @@ export function SnapText({
|
|
|
40
48
|
*/
|
|
41
49
|
inHorizontalStack ? "min-w-0 shrink" : "min-w-0",
|
|
42
50
|
)}
|
|
43
|
-
style={{
|
|
51
|
+
style={{
|
|
52
|
+
color: colors.text,
|
|
53
|
+
lineHeight: size === "sm" ? 1.35 : 1.4,
|
|
54
|
+
...(maxLines
|
|
55
|
+
? {
|
|
56
|
+
display: "-webkit-box",
|
|
57
|
+
WebkitBoxOrient: "vertical",
|
|
58
|
+
WebkitLineClamp: maxLines,
|
|
59
|
+
overflow: "hidden",
|
|
60
|
+
}
|
|
61
|
+
: {}),
|
|
62
|
+
}}
|
|
44
63
|
>
|
|
45
64
|
{content}
|
|
46
65
|
</Text>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import type { SpecVersion } from "../constants.js";
|
|
5
|
+
|
|
6
|
+
const SnapVersionContext = createContext<SpecVersion>("1.0");
|
|
7
|
+
|
|
8
|
+
export const SnapVersionProvider = SnapVersionContext.Provider;
|
|
9
|
+
|
|
10
|
+
export function useSnapVersion(): SpecVersion {
|
|
11
|
+
return useContext(SnapVersionContext);
|
|
12
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Spec } from "@json-render/core";
|
|
|
4
4
|
import { snapJsonRenderCatalog } from "../ui/index.js";
|
|
5
5
|
import { SnapCatalogView } from "./catalog-renderer";
|
|
6
6
|
import { SnapPreviewAccentProvider } from "./accent-context";
|
|
7
|
+
import { SnapVersionProvider } from "./snap-version-context";
|
|
7
8
|
import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex";
|
|
8
9
|
import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css";
|
|
9
10
|
import {
|
|
@@ -21,8 +22,13 @@ import type { JsonValue, SnapActionHandlers, SnapPage } from "./index";
|
|
|
21
22
|
|
|
22
23
|
export function applyStatePaths(
|
|
23
24
|
model: Record<string, unknown>,
|
|
24
|
-
changes:
|
|
25
|
+
changes:
|
|
26
|
+
| { path: string; value: unknown }[]
|
|
27
|
+
| Record<string, unknown>
|
|
28
|
+
| null
|
|
29
|
+
| undefined,
|
|
25
30
|
): void {
|
|
31
|
+
if (!changes) return;
|
|
26
32
|
const entries = Array.isArray(changes)
|
|
27
33
|
? changes.map((c) => [c.path, c.value] as const)
|
|
28
34
|
: Object.entries(changes);
|
|
@@ -53,6 +59,30 @@ export function applyStatePaths(
|
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
function withDefaultElementProps(spec: Spec): Spec {
|
|
63
|
+
if (!spec || typeof spec !== "object" || !("elements" in spec)) return spec;
|
|
64
|
+
const elements = spec.elements as unknown as Record<
|
|
65
|
+
string,
|
|
66
|
+
Record<string, unknown>
|
|
67
|
+
>;
|
|
68
|
+
if (!elements || typeof elements !== "object") return spec;
|
|
69
|
+
|
|
70
|
+
let changed = false;
|
|
71
|
+
const nextElements: Record<string, Record<string, unknown>> = {};
|
|
72
|
+
for (const [id, element] of Object.entries(elements)) {
|
|
73
|
+
if (element.props !== undefined) {
|
|
74
|
+
nextElements[id] = element;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
changed = true;
|
|
78
|
+
nextElements[id] = { ...element, props: {} };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return changed
|
|
82
|
+
? ({ ...spec, elements: nextElements } as unknown as Spec)
|
|
83
|
+
: spec;
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
const CONFETTI_COLORS = [
|
|
57
87
|
"#907AA9",
|
|
58
88
|
"#EC4899",
|
|
@@ -330,7 +360,7 @@ export function SnapViewCore({
|
|
|
330
360
|
*/
|
|
331
361
|
loadingOverlay?: ReactNode;
|
|
332
362
|
}) {
|
|
333
|
-
const spec = snap.ui;
|
|
363
|
+
const spec = useMemo(() => withDefaultElementProps(snap.ui), [snap.ui]);
|
|
334
364
|
const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
|
|
335
365
|
|
|
336
366
|
const stateRef = useRef<Record<string, unknown>>(initialState);
|
|
@@ -465,16 +495,18 @@ export function SnapViewCore({
|
|
|
465
495
|
pageAccent={snap.theme?.accent}
|
|
466
496
|
appearance={appearance}
|
|
467
497
|
>
|
|
468
|
-
<
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
498
|
+
<SnapVersionProvider value={snap.version === "2.0" ? "2.0" : "1.0"}>
|
|
499
|
+
<SnapCatalogView
|
|
500
|
+
key={pageKey}
|
|
501
|
+
spec={spec}
|
|
502
|
+
state={initialState}
|
|
503
|
+
loading={false}
|
|
504
|
+
onStateChange={(changes) => {
|
|
505
|
+
applyStatePaths(stateRef.current, changes);
|
|
506
|
+
}}
|
|
507
|
+
onAction={handleAction}
|
|
508
|
+
/>
|
|
509
|
+
</SnapVersionProvider>
|
|
478
510
|
</SnapPreviewAccentProvider>
|
|
479
511
|
</div>
|
|
480
512
|
</div>
|
|
@@ -7,6 +7,7 @@ import { SnapImage } from "./components/snap-image";
|
|
|
7
7
|
import { SnapInput } from "./components/snap-input";
|
|
8
8
|
import { SnapItem } from "./components/snap-item";
|
|
9
9
|
import { SnapItemGroup } from "./components/snap-item-group";
|
|
10
|
+
import { SnapPaginator } from "./components/snap-paginator";
|
|
10
11
|
import { SnapProgress } from "./components/snap-progress";
|
|
11
12
|
import { SnapSeparator } from "./components/snap-separator";
|
|
12
13
|
import { SnapSlider } from "./components/snap-slider";
|
|
@@ -29,6 +30,7 @@ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
|
|
|
29
30
|
input: SnapInput,
|
|
30
31
|
item: SnapItem,
|
|
31
32
|
item_group: SnapItemGroup,
|
|
33
|
+
paginator: SnapPaginator,
|
|
32
34
|
progress: SnapProgress,
|
|
33
35
|
separator: SnapSeparator,
|
|
34
36
|
slider: SnapSlider,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
declare const __DEV__: boolean;
|
|
2
2
|
|
|
3
3
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
4
|
+
import { useStateStore } from "@json-render/react-native";
|
|
4
5
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
5
6
|
import { ExternalLink } from "lucide-react-native";
|
|
6
7
|
import { useSnapPalette } from "../use-snap-palette";
|
|
7
8
|
import { useSnapTheme } from "../theme";
|
|
9
|
+
import {
|
|
10
|
+
getPaginatorAction,
|
|
11
|
+
runPaginatorAction,
|
|
12
|
+
} from "../../ui/paginator-state";
|
|
8
13
|
import { useSnapStackDirection } from "../stack-direction-context";
|
|
9
14
|
import { ICON_MAP } from "./snap-icon";
|
|
10
15
|
|
|
@@ -37,6 +42,8 @@ export function SnapActionButton({
|
|
|
37
42
|
|
|
38
43
|
const on = (element as unknown as { on?: Record<string, unknown> }).on;
|
|
39
44
|
const showExternalIcon = isExternalLinkAction(on);
|
|
45
|
+
const stateStore = useStateStore();
|
|
46
|
+
const paginatorAction = getPaginatorAction(on);
|
|
40
47
|
|
|
41
48
|
return (
|
|
42
49
|
<View style={inHorizontalStack ? styles.outerHorizontal : styles.outer}>
|
|
@@ -50,6 +57,7 @@ export function SnapActionButton({
|
|
|
50
57
|
pressed && styles.pressed,
|
|
51
58
|
]}
|
|
52
59
|
onPress={() => {
|
|
60
|
+
if (runPaginatorAction(stateStore, paginatorAction)) return;
|
|
53
61
|
void (async () => {
|
|
54
62
|
try {
|
|
55
63
|
await emit("press");
|
|
@@ -100,10 +108,10 @@ const styles = StyleSheet.create({
|
|
|
100
108
|
gap: 8,
|
|
101
109
|
},
|
|
102
110
|
btnDefault: {
|
|
103
|
-
paddingVertical:
|
|
111
|
+
paddingVertical: 8,
|
|
104
112
|
},
|
|
105
113
|
btnOther: {
|
|
106
|
-
paddingVertical:
|
|
114
|
+
paddingVertical: 6,
|
|
107
115
|
},
|
|
108
116
|
pressed: { opacity: 0.88 },
|
|
109
117
|
});
|
|
@@ -4,6 +4,10 @@ import { useStateStore } from "@json-render/react-native";
|
|
|
4
4
|
import { useSnapPalette } from "../use-snap-palette";
|
|
5
5
|
import { useSnapTheme } from "../theme";
|
|
6
6
|
import { POST_GRID_TAP_KEY, readableTextOnHex } from "@farcaster/snap";
|
|
7
|
+
import {
|
|
8
|
+
getPaginatorAction,
|
|
9
|
+
runPaginatorAction,
|
|
10
|
+
} from "../../ui/paginator-state";
|
|
7
11
|
|
|
8
12
|
export function SnapCellGrid({
|
|
9
13
|
element,
|
|
@@ -13,12 +17,21 @@ export function SnapCellGrid({
|
|
|
13
17
|
const on = (element as unknown as { on?: Record<string, unknown> }).on;
|
|
14
18
|
const { hex, appearance } = useSnapPalette();
|
|
15
19
|
const { colors } = useSnapTheme();
|
|
16
|
-
const
|
|
20
|
+
const stateStore = useStateStore();
|
|
21
|
+
const { get, set } = stateStore;
|
|
22
|
+
const paginatorAction = getPaginatorAction(on);
|
|
17
23
|
const cols = Number(props.cols ?? 2);
|
|
18
24
|
const rows = Number(props.rows ?? 2);
|
|
19
25
|
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
20
26
|
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
21
27
|
const squareCells = props.cellAspectRatio === "square";
|
|
28
|
+
const maxWidthKey = typeof props.maxWidth === "string" ? props.maxWidth : undefined;
|
|
29
|
+
const maxWidthMap: Record<string, number | undefined> = {
|
|
30
|
+
sm: 160,
|
|
31
|
+
md: 220,
|
|
32
|
+
lg: undefined,
|
|
33
|
+
};
|
|
34
|
+
const maxWidth = maxWidthKey ? maxWidthMap[maxWidthKey] : undefined;
|
|
22
35
|
const gap = String(props.gap ?? "sm");
|
|
23
36
|
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
24
37
|
const gapPx = gapMap[gap] ?? 1;
|
|
@@ -75,7 +88,12 @@ export function SnapCellGrid({
|
|
|
75
88
|
} else {
|
|
76
89
|
set(tapPath, wire);
|
|
77
90
|
}
|
|
78
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
hasPressAction &&
|
|
93
|
+
!runPaginatorAction(stateStore, paginatorAction)
|
|
94
|
+
) {
|
|
95
|
+
emit("press");
|
|
96
|
+
}
|
|
79
97
|
};
|
|
80
98
|
|
|
81
99
|
const ringOuter = appearance === "dark" ? "#fff" : "#000";
|
|
@@ -168,6 +186,8 @@ export function SnapCellGrid({
|
|
|
168
186
|
style={[
|
|
169
187
|
styles.wrap,
|
|
170
188
|
{
|
|
189
|
+
maxWidth,
|
|
190
|
+
alignSelf: maxWidth ? "center" : undefined,
|
|
171
191
|
gap: gapPx,
|
|
172
192
|
backgroundColor: colors.muted,
|
|
173
193
|
padding: 4,
|