@farcaster/snap 2.0.3 → 2.1.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/react/components/cell-grid.d.ts +3 -1
- package/dist/react/components/cell-grid.js +8 -4
- package/dist/react/snap-view-core.js +7 -2
- package/dist/react/v1/snap-view.js +40 -34
- package/dist/react/v2/snap-view.js +92 -29
- package/dist/react-native/components/snap-cell-grid.d.ts +1 -1
- package/dist/react-native/components/snap-cell-grid.js +10 -4
- package/dist/react-native/confetti-overlay.js +33 -36
- package/dist/react-native/snap-view-core.js +8 -1
- package/dist/react-native/v1/snap-view.js +41 -47
- package/dist/react-native/v2/snap-view.js +78 -16
- package/dist/ui/catalog.js +1 -1
- package/dist/validator.js +8 -33
- package/llms.txt +22 -1
- package/package.json +1 -1
- package/src/react/components/cell-grid.tsx +11 -5
- package/src/react/snap-view-core.tsx +6 -2
- package/src/react/v1/snap-view.tsx +69 -63
- package/src/react/v2/snap-view.tsx +152 -61
- package/src/react-native/components/snap-cell-grid.tsx +11 -4
- package/src/react-native/confetti-overlay.tsx +40 -37
- package/src/react-native/snap-view-core.tsx +8 -0
- package/src/react-native/v1/snap-view.tsx +34 -42
- package/src/react-native/v2/snap-view.tsx +131 -30
- package/src/ui/catalog.ts +1 -1
- package/src/validator.ts +22 -46
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { SnapThemeProvider, useSnapTheme } from "../theme.js";
|
|
5
5
|
import { SnapLoadingOverlay, SnapViewCoreInner, resolveAccentHex, } from "../snap-view-core.js";
|
|
6
6
|
import { validateSnapResponse, } from "@farcaster/snap";
|
|
@@ -54,28 +54,63 @@ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWa
|
|
|
54
54
|
const { colors, mode } = useSnapTheme();
|
|
55
55
|
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
56
56
|
const [contentHeight, setContentHeight] = useState(0);
|
|
57
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setIsExpanded(false);
|
|
60
|
+
setContentHeight(0);
|
|
61
|
+
}, [snap]);
|
|
62
|
+
const isExpandable = !showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT + 1;
|
|
63
|
+
const isClipped = isExpandable && !isExpanded;
|
|
57
64
|
const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null }));
|
|
58
65
|
if (plain) {
|
|
59
|
-
return (_jsxs(_Fragment, { children: [
|
|
66
|
+
return (_jsxs(_Fragment, { children: [_jsx(View, { style: isClipped ? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" } : undefined, children: _jsx(View, { collapsable: false, onLayout: (e) => {
|
|
67
|
+
const nextHeight = Math.round(e.nativeEvent.layout.height);
|
|
68
|
+
setContentHeight((current) => isClipped
|
|
69
|
+
? Math.max(current, nextHeight)
|
|
70
|
+
: current === nextHeight
|
|
71
|
+
? current
|
|
72
|
+
: nextHeight);
|
|
73
|
+
}, children: content }) }), loading
|
|
60
74
|
? loadingOverlay === undefined
|
|
61
75
|
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
62
76
|
: loadingOverlay
|
|
63
|
-
: null] })
|
|
77
|
+
: null, isExpandable ? (_jsx(View, { style: [cardStyles.expandRow, cardStyles.expandRowPlain], children: _jsx(Pressable, { style: ({ pressed }) => [
|
|
78
|
+
cardStyles.expandButton,
|
|
79
|
+
{
|
|
80
|
+
backgroundColor: pressed ? colors.mutedHover : colors.muted,
|
|
81
|
+
},
|
|
82
|
+
], onPress: () => setIsExpanded((value) => !value), children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: isExpanded ? "Show less" : "Show more" }) }) })) : null] }));
|
|
64
83
|
}
|
|
65
84
|
const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
const isDark = mode === "dark";
|
|
86
|
+
const pillBg = isDark ? "rgba(40,40,40,0.92)" : "rgba(255,255,255,0.92)";
|
|
87
|
+
const pillBgPressed = isDark ? "rgba(60,60,60,0.95)" : "rgba(240,240,240,0.95)";
|
|
88
|
+
return (_jsxs(_Fragment, { children: [_jsxs(View, { style: { position: "relative" }, children: [_jsxs(View, { style: {
|
|
89
|
+
borderRadius,
|
|
90
|
+
borderWidth: 1,
|
|
91
|
+
borderColor: colors.border,
|
|
92
|
+
backgroundColor: colors.surface,
|
|
93
|
+
maxHeight: showOverflowWarning ? undefined : isClipped ? SNAP_MAX_HEIGHT : undefined,
|
|
94
|
+
overflow: "hidden",
|
|
95
|
+
minHeight: 120,
|
|
96
|
+
}, children: [_jsx(View, { collapsable: false, onLayout: (e) => {
|
|
97
|
+
const nextHeight = Math.round(e.nativeEvent.layout.height);
|
|
98
|
+
setContentHeight((current) => isClipped
|
|
99
|
+
? Math.max(current, nextHeight)
|
|
100
|
+
: current === nextHeight
|
|
101
|
+
? current
|
|
102
|
+
: nextHeight);
|
|
103
|
+
}, style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, height: overflowAmount, zIndex: 10, pointerEvents: "none" }, children: [_jsx(View, { style: { height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" } }), _jsx(View, { style: { position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }, children: _jsxs(Text, { style: { fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx(View, { style: { flex: 1, backgroundColor: "rgba(255,50,50,0.15)" } })] })), loading
|
|
104
|
+
? loadingOverlay === undefined
|
|
105
|
+
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
106
|
+
: loadingOverlay
|
|
107
|
+
: null] }), isExpandable ? (_jsx(View, { pointerEvents: "box-none", style: cardStyles.expandFloat, children: _jsx(Pressable, { style: ({ pressed }) => [
|
|
108
|
+
cardStyles.expandButton,
|
|
109
|
+
{
|
|
110
|
+
backgroundColor: pressed ? pillBgPressed : pillBg,
|
|
111
|
+
borderColor: colors.border,
|
|
112
|
+
},
|
|
113
|
+
], onPress: () => setIsExpanded((value) => !value), children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: isExpanded ? "Show less" : "Show more" }) }) })) : null] }), actionError && (_jsx(Text, { style: {
|
|
79
114
|
paddingHorizontal: 12,
|
|
80
115
|
paddingVertical: 8,
|
|
81
116
|
fontSize: 13,
|
|
@@ -92,6 +127,33 @@ const cardStyles = StyleSheet.create({
|
|
|
92
127
|
card: { borderWidth: 1, minHeight: 120, overflow: "hidden" },
|
|
93
128
|
body: { paddingHorizontal: 16, paddingVertical: 16 },
|
|
94
129
|
actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
|
|
130
|
+
expandFloat: {
|
|
131
|
+
position: "absolute",
|
|
132
|
+
left: 0,
|
|
133
|
+
right: 0,
|
|
134
|
+
bottom: -14,
|
|
135
|
+
height: 28,
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
justifyContent: "center",
|
|
138
|
+
},
|
|
139
|
+
expandRowPlain: {
|
|
140
|
+
paddingTop: 8,
|
|
141
|
+
alignItems: "center",
|
|
142
|
+
},
|
|
143
|
+
expandButton: {
|
|
144
|
+
minWidth: 92,
|
|
145
|
+
alignItems: "center",
|
|
146
|
+
justifyContent: "center",
|
|
147
|
+
borderRadius: 9999,
|
|
148
|
+
borderWidth: 1,
|
|
149
|
+
paddingHorizontal: 10,
|
|
150
|
+
paddingVertical: 4,
|
|
151
|
+
},
|
|
152
|
+
expandButtonText: {
|
|
153
|
+
fontSize: 12,
|
|
154
|
+
lineHeight: 16,
|
|
155
|
+
fontWeight: "600",
|
|
156
|
+
},
|
|
95
157
|
warningOverlay: {
|
|
96
158
|
position: "absolute",
|
|
97
159
|
top: SNAP_MAX_HEIGHT,
|
package/dist/ui/catalog.js
CHANGED
|
@@ -90,7 +90,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
90
90
|
},
|
|
91
91
|
cell_grid: {
|
|
92
92
|
props: cellGridProps,
|
|
93
|
-
description: "Cell grid — sparse colored cells on a rows×cols grid.
|
|
93
|
+
description: "Cell grid — sparse colored cells on a rows×cols grid. Two interaction modes: leave select 'off' and bind on.press to fire an action per cell press (inputs[name] is the pressed 'row,col' before the action runs); or set select 'single'/'multiple' for press-to-select with a visual ring (no auto-fire — pair with a separate submit button). on.press is ignored when select is on.",
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
96
|
actions: {
|
package/dist/validator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { snapResponseSchema } from "./schemas.js";
|
|
2
|
-
import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_1 } from "./constants.js";
|
|
2
|
+
import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_1, } from "./constants.js";
|
|
3
3
|
import { snapJsonRenderCatalog } from "./ui/catalog.js";
|
|
4
4
|
// ─── Helpers ──────────────────────────────────────────
|
|
5
5
|
/** Actions whose `params.target` must be a valid URL. */
|
|
@@ -9,8 +9,6 @@ const URL_TARGET_ACTIONS = new Set([
|
|
|
9
9
|
"open_snap",
|
|
10
10
|
"open_mini_app",
|
|
11
11
|
]);
|
|
12
|
-
/** Image file extensions allowed in image URLs. */
|
|
13
|
-
const ALLOWED_IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "gif", "webp"]);
|
|
14
12
|
/**
|
|
15
13
|
* Returns true if the URL is a loopback address (localhost dev exception).
|
|
16
14
|
*/
|
|
@@ -38,31 +36,6 @@ function validateUrl(raw) {
|
|
|
38
36
|
return `javascript: URIs are not allowed`;
|
|
39
37
|
return `URL must use HTTPS (got ${url.protocol.replace(":", "")}): "${raw}"`;
|
|
40
38
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Validate an image URL: must pass URL validation + have an allowed extension.
|
|
43
|
-
*/
|
|
44
|
-
function validateImageUrl(raw) {
|
|
45
|
-
const urlError = validateUrl(raw);
|
|
46
|
-
if (urlError)
|
|
47
|
-
return urlError;
|
|
48
|
-
let url;
|
|
49
|
-
try {
|
|
50
|
-
url = new URL(raw);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return null; // already caught above
|
|
54
|
-
}
|
|
55
|
-
const pathname = url.pathname;
|
|
56
|
-
const lastDot = pathname.lastIndexOf(".");
|
|
57
|
-
if (lastDot === -1) {
|
|
58
|
-
return `Image URL must end with a supported extension (${[...ALLOWED_IMAGE_EXTENSIONS].join(", ")}): "${raw}"`;
|
|
59
|
-
}
|
|
60
|
-
const ext = pathname.slice(lastDot + 1).toLowerCase();
|
|
61
|
-
if (!ALLOWED_IMAGE_EXTENSIONS.has(ext)) {
|
|
62
|
-
return `Image URL has unsupported extension ".${ext}" (allowed: ${[...ALLOWED_IMAGE_EXTENSIONS].join(", ")}): "${raw}"`;
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
39
|
// ─── Depth measurement ────────────────────────────────
|
|
67
40
|
/**
|
|
68
41
|
* Walk the element tree from `root` and return the max depth reached.
|
|
@@ -133,8 +106,8 @@ function validateStructure(ui) {
|
|
|
133
106
|
// ─── URL validation ───────────────────────────────────
|
|
134
107
|
/**
|
|
135
108
|
* Validate all URLs in the snap:
|
|
136
|
-
* - image.url: must
|
|
137
|
-
* - action target URLs (submit, open_url, open_snap, open_mini_app): must
|
|
109
|
+
* - image.url: must use HTTPS (or HTTP on loopback for dev)
|
|
110
|
+
* - action target URLs (submit, open_url, open_snap, open_mini_app): must use HTTPS (or HTTP on loopback for dev)
|
|
138
111
|
*/
|
|
139
112
|
function validateUrls(elements) {
|
|
140
113
|
const issues = [];
|
|
@@ -142,7 +115,7 @@ function validateUrls(elements) {
|
|
|
142
115
|
for (const [id, el] of Object.entries(els)) {
|
|
143
116
|
// Validate image URLs
|
|
144
117
|
if (el.type === "image" && typeof el.props?.url === "string") {
|
|
145
|
-
const error =
|
|
118
|
+
const error = validateUrl(el.props.url);
|
|
146
119
|
if (error) {
|
|
147
120
|
issues.push({
|
|
148
121
|
code: "custom",
|
|
@@ -191,11 +164,13 @@ export function validateSnapResponse(json) {
|
|
|
191
164
|
if (!(ui.root in ui.elements)) {
|
|
192
165
|
return {
|
|
193
166
|
valid: false,
|
|
194
|
-
issues: [
|
|
167
|
+
issues: [
|
|
168
|
+
{
|
|
195
169
|
code: "custom",
|
|
196
170
|
message: `ui.root "${ui.root}" does not exist in ui.elements`,
|
|
197
171
|
path: ["ui", "root"],
|
|
198
|
-
}
|
|
172
|
+
},
|
|
173
|
+
],
|
|
199
174
|
};
|
|
200
175
|
}
|
|
201
176
|
// Structural limits and URL validation only apply to v2+ snaps
|
package/llms.txt
CHANGED
|
@@ -98,7 +98,8 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
|
|
|
98
98
|
- `cells` (array, required): sparse list of `{ row, col, color?: PaletteColor, content?: string }`
|
|
99
99
|
- `gap` (optional): `"none"` (0px) | `"sm"` (1px) | `"md"` (2px) | `"lg"` (4px). Default: `"sm"`
|
|
100
100
|
- `rowHeight` (number, optional, 8–64): pixel height per row. Default: 28. Grid height = rows × rowHeight
|
|
101
|
-
- `select` (optional): `"off"` | `"single"` | `"multiple"`. Default: `"off"`.
|
|
101
|
+
- `select` (optional): `"off"` | `"single"` | `"multiple"`. Default: `"off"`. With `select: "off"`, bind `on.press` for press-to-act (each press writes `"row,col"` to `inputs[name]` and fires the action). With `"single"` / `"multiple"`, presses accumulate selection state and pair with a separate submit `button`; `on.press` is ignored.
|
|
102
|
+
- Events: `press` — fires on cell press, only when `select: "off"`; `inputs[name]` is set to `"row,col"` before the bound action runs
|
|
102
103
|
|
|
103
104
|
### Container Components
|
|
104
105
|
|
|
@@ -196,6 +197,26 @@ import { withTursoServerless, createInMemoryDataStore } from "@farcaster/snap-tu
|
|
|
196
197
|
- `@farcaster/snap-hono` — Hono adapter (`registerSnapHandler`)
|
|
197
198
|
- `@farcaster/snap-turso` — `withTursoServerless`, `DataStore` / `DataStoreValue`, in-memory and Turso helpers
|
|
198
199
|
|
|
200
|
+
## Template Project Setup
|
|
201
|
+
|
|
202
|
+
The `template/` directory is the starting point for a snap. It's an ESM project
|
|
203
|
+
(`"type": "module"`) with `moduleResolution: "NodeNext"`.
|
|
204
|
+
|
|
205
|
+
**ESM import rule (CRITICAL)**: all local relative imports must include the `.js`
|
|
206
|
+
extension, even though the source files are `.ts`:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// ✅ correct
|
|
210
|
+
import { foo } from "./foo.js";
|
|
211
|
+
|
|
212
|
+
// ❌ wrong — fails `pnpm build`; on deploy every route returns 500 FUNCTION_INVOCATION_FAILED
|
|
213
|
+
import { foo } from "./foo";
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`tsx` dev accepts bare imports, so the bug only surfaces on deploy. Always run
|
|
217
|
+
`pnpm build` (`tsc --noEmit`) before deploying — NodeNext makes this a build-time error.
|
|
218
|
+
Bare package imports (`hono`, `@farcaster/snap`, etc.) do not need an extension.
|
|
219
|
+
|
|
199
220
|
## Full Documentation
|
|
200
221
|
|
|
201
222
|
https://docs.farcaster.xyz/snap
|
package/package.json
CHANGED
|
@@ -7,17 +7,21 @@ import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
|
7
7
|
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
8
8
|
|
|
9
9
|
export function SnapCellGrid({
|
|
10
|
-
element: { props },
|
|
10
|
+
element: { props, on },
|
|
11
|
+
emit,
|
|
11
12
|
}: {
|
|
12
|
-
element: { props: Record<string, unknown> };
|
|
13
|
+
element: { props: Record<string, unknown>; on?: Record<string, unknown> };
|
|
14
|
+
emit: (name: string) => void;
|
|
13
15
|
}) {
|
|
14
16
|
const { get, set } = useStateStore();
|
|
15
17
|
const colors = useSnapColors();
|
|
16
18
|
const cols = Number(props.cols ?? 2);
|
|
17
19
|
const rows = Number(props.rows ?? 2);
|
|
18
20
|
const select = String(props.select ?? "off");
|
|
19
|
-
const interactive = select !== "off";
|
|
20
21
|
const isMultiple = select === "multiple";
|
|
22
|
+
const isSelectable = select !== "off";
|
|
23
|
+
const hasPressAction = Boolean(on?.press);
|
|
24
|
+
const interactive = isSelectable || hasPressAction;
|
|
21
25
|
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
22
26
|
const gap = String(props.gap ?? "sm");
|
|
23
27
|
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
@@ -36,7 +40,8 @@ export function SnapCellGrid({
|
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
const isSelected = (r: number, c: number) =>
|
|
43
|
+
const isSelected = (r: number, c: number) =>
|
|
44
|
+
isSelectable && selectedSet.has(`${r},${c}`);
|
|
40
45
|
|
|
41
46
|
const handleTap = (r: number, c: number) => {
|
|
42
47
|
const key = `${r},${c}`;
|
|
@@ -48,6 +53,7 @@ export function SnapCellGrid({
|
|
|
48
53
|
} else {
|
|
49
54
|
set(tapPath, key);
|
|
50
55
|
}
|
|
56
|
+
if (hasPressAction) emit("press");
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
const cellMap = new Map<string, { color?: string; content?: string }>();
|
|
@@ -99,7 +105,7 @@ export function SnapCellGrid({
|
|
|
99
105
|
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
const selectionLabel =
|
|
108
|
+
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
103
109
|
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
104
110
|
: null;
|
|
105
111
|
|
|
@@ -239,7 +239,11 @@ export function SnapViewCore({
|
|
|
239
239
|
setPageKey((k) => k + 1);
|
|
240
240
|
}, [spec]);
|
|
241
241
|
|
|
242
|
-
const showConfetti = snap.effects?.includes("confetti");
|
|
242
|
+
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
243
|
+
const [confettiKey, setConfettiKey] = useState(0);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (showConfetti) setConfettiKey((k) => k + 1);
|
|
246
|
+
}, [showConfetti, snap]);
|
|
243
247
|
|
|
244
248
|
const accentName = snap.theme?.accent ?? "purple";
|
|
245
249
|
|
|
@@ -321,7 +325,7 @@ export function SnapViewCore({
|
|
|
321
325
|
|
|
322
326
|
return (
|
|
323
327
|
<div style={{ position: "relative", width: "100%" }}>
|
|
324
|
-
{showConfetti && <ConfettiOverlay />}
|
|
328
|
+
{showConfetti && <ConfettiOverlay key={confettiKey} />}
|
|
325
329
|
{loadingOverlay === undefined ? (
|
|
326
330
|
<SnapLoadingOverlay
|
|
327
331
|
appearance={appearance}
|
|
@@ -105,80 +105,86 @@ export function SnapCardV1({
|
|
|
105
105
|
position: "relative",
|
|
106
106
|
width: "100%",
|
|
107
107
|
maxWidth,
|
|
108
|
-
overflow: "hidden",
|
|
109
|
-
...(plain ? {} : {
|
|
110
|
-
borderRadius: 16,
|
|
111
|
-
border: `1px solid ${borderColor}`,
|
|
112
|
-
backgroundColor: surfaceBg,
|
|
113
|
-
}),
|
|
114
108
|
}}
|
|
115
109
|
>
|
|
116
110
|
<div
|
|
117
|
-
style={
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:
|
|
124
|
-
|
|
111
|
+
style={{
|
|
112
|
+
position: "relative",
|
|
113
|
+
overflow: "hidden",
|
|
114
|
+
...(plain ? {} : {
|
|
115
|
+
borderRadius: 16,
|
|
116
|
+
border: `1px solid ${borderColor}`,
|
|
117
|
+
backgroundColor: surfaceBg,
|
|
118
|
+
}),
|
|
119
|
+
}}
|
|
125
120
|
>
|
|
126
|
-
<div
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
<div
|
|
122
|
+
style={
|
|
123
|
+
isClipped
|
|
124
|
+
? {
|
|
125
|
+
maxHeight: SNAP_MAX_HEIGHT,
|
|
126
|
+
overflow: "hidden",
|
|
127
|
+
}
|
|
128
|
+
: undefined
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
<div ref={contentRef} style={plain ? undefined : { padding: 16 }}>
|
|
132
|
+
<SnapViewV1
|
|
133
|
+
snap={snap}
|
|
134
|
+
handlers={handlers}
|
|
135
|
+
loading={loading}
|
|
136
|
+
appearance={appearance}
|
|
137
|
+
loadingOverlay={null}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{loadingOverlay === undefined ? (
|
|
142
|
+
<SnapLoadingOverlay
|
|
131
143
|
appearance={appearance}
|
|
132
|
-
|
|
144
|
+
accentHex={accentHex}
|
|
145
|
+
active={loading}
|
|
133
146
|
/>
|
|
134
|
-
|
|
147
|
+
) : loading ? (
|
|
148
|
+
<>{loadingOverlay}</>
|
|
149
|
+
) : null}
|
|
135
150
|
</div>
|
|
136
|
-
{loadingOverlay === undefined ? (
|
|
137
|
-
<SnapLoadingOverlay
|
|
138
|
-
appearance={appearance}
|
|
139
|
-
accentHex={accentHex}
|
|
140
|
-
active={loading}
|
|
141
|
-
/>
|
|
142
|
-
) : loading ? (
|
|
143
|
-
<>{loadingOverlay}</>
|
|
144
|
-
) : null}
|
|
145
151
|
{isExpandable ? (
|
|
146
|
-
<
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
aria-expanded={isExpanded}
|
|
155
|
+
onClick={() => setIsExpanded((value) => !value)}
|
|
147
156
|
style={{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
position: "absolute",
|
|
158
|
+
bottom: 0,
|
|
159
|
+
left: "50%",
|
|
160
|
+
transform: "translate(-50%, 50%)",
|
|
161
|
+
appearance: "none",
|
|
162
|
+
border: `1px solid ${borderColor}`,
|
|
163
|
+
borderRadius: 9999,
|
|
164
|
+
backgroundColor: isDark ? "rgba(30,30,30,0.6)" : "rgba(255,255,255,0.6)",
|
|
165
|
+
backdropFilter: "blur(12px) saturate(180%)",
|
|
166
|
+
WebkitBackdropFilter: "blur(12px) saturate(180%)",
|
|
167
|
+
color: toggleText,
|
|
168
|
+
padding: "2px 10px",
|
|
169
|
+
fontSize: 12,
|
|
170
|
+
lineHeight: "16px",
|
|
171
|
+
fontWeight: 600,
|
|
172
|
+
cursor: "pointer",
|
|
173
|
+
zIndex: 11,
|
|
174
|
+
}}
|
|
175
|
+
onMouseEnter={(event) => {
|
|
176
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
177
|
+
? "rgba(50,50,50,0.7)"
|
|
178
|
+
: "rgba(245,245,245,0.75)";
|
|
179
|
+
}}
|
|
180
|
+
onMouseLeave={(event) => {
|
|
181
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
182
|
+
? "rgba(30,30,30,0.6)"
|
|
183
|
+
: "rgba(255,255,255,0.6)";
|
|
154
184
|
}}
|
|
155
185
|
>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
aria-expanded={isExpanded}
|
|
159
|
-
onClick={() => setIsExpanded((value) => !value)}
|
|
160
|
-
style={{
|
|
161
|
-
appearance: "none",
|
|
162
|
-
border: "none",
|
|
163
|
-
borderRadius: 9999,
|
|
164
|
-
backgroundColor: toggleBg,
|
|
165
|
-
color: toggleText,
|
|
166
|
-
padding: "6px 10px",
|
|
167
|
-
fontSize: 13,
|
|
168
|
-
lineHeight: "18px",
|
|
169
|
-
fontWeight: 600,
|
|
170
|
-
cursor: "pointer",
|
|
171
|
-
}}
|
|
172
|
-
onMouseEnter={(event) => {
|
|
173
|
-
event.currentTarget.style.backgroundColor = toggleBgHover;
|
|
174
|
-
}}
|
|
175
|
-
onMouseLeave={(event) => {
|
|
176
|
-
event.currentTarget.style.backgroundColor = toggleBg;
|
|
177
|
-
}}
|
|
178
|
-
>
|
|
179
|
-
{isExpanded ? "Show less" : "Show more"}
|
|
180
|
-
</button>
|
|
181
|
-
</div>
|
|
186
|
+
{isExpanded ? "Show less" : "Show more"}
|
|
187
|
+
</button>
|
|
182
188
|
) : null}
|
|
183
189
|
{actionError && (
|
|
184
190
|
<div
|