@farcaster/snap 2.0.2 → 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.
Files changed (38) hide show
  1. package/dist/react/components/cell-grid.d.ts +3 -1
  2. package/dist/react/components/cell-grid.js +8 -4
  3. package/dist/react/index.d.ts +3 -1
  4. package/dist/react/index.js +3 -3
  5. package/dist/react/snap-view-core.d.ts +12 -1
  6. package/dist/react/snap-view-core.js +10 -5
  7. package/dist/react/v1/snap-view.d.ts +7 -2
  8. package/dist/react/v1/snap-view.js +48 -40
  9. package/dist/react/v2/snap-view.d.ts +6 -2
  10. package/dist/react/v2/snap-view.js +98 -33
  11. package/dist/react-native/components/snap-cell-grid.d.ts +1 -1
  12. package/dist/react-native/components/snap-cell-grid.js +10 -4
  13. package/dist/react-native/confetti-overlay.js +33 -36
  14. package/dist/react-native/index.d.ts +3 -1
  15. package/dist/react-native/index.js +3 -3
  16. package/dist/react-native/snap-view-core.d.ts +11 -1
  17. package/dist/react-native/snap-view-core.js +25 -9
  18. package/dist/react-native/v1/snap-view.d.ts +9 -3
  19. package/dist/react-native/v1/snap-view.js +51 -52
  20. package/dist/react-native/v2/snap-view.d.ts +8 -3
  21. package/dist/react-native/v2/snap-view.js +92 -21
  22. package/dist/ui/catalog.js +2 -2
  23. package/dist/validator.js +8 -33
  24. package/llms.txt +26 -3
  25. package/package.json +1 -1
  26. package/src/react/components/cell-grid.tsx +11 -5
  27. package/src/react/index.tsx +5 -0
  28. package/src/react/snap-view-core.tsx +23 -8
  29. package/src/react/v1/snap-view.tsx +84 -55
  30. package/src/react/v2/snap-view.tsx +165 -52
  31. package/src/react-native/components/snap-cell-grid.tsx +11 -4
  32. package/src/react-native/confetti-overlay.tsx +40 -37
  33. package/src/react-native/index.tsx +5 -0
  34. package/src/react-native/snap-view-core.tsx +56 -14
  35. package/src/react-native/v1/snap-view.tsx +71 -47
  36. package/src/react-native/v2/snap-view.tsx +166 -28
  37. package/src/ui/catalog.ts +2 -2
  38. package/src/validator.ts +22 -46
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 be HTTPS with allowed extension
137
- * - action target URLs (submit, open_url, open_snap, open_mini_app): must be HTTPS
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 = validateImageUrl(el.props.url);
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
@@ -68,7 +68,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
68
68
  - `title` (string, required, max 100)
69
69
  - `description` (string, optional, max 160)
70
70
  - `variant` (optional): `"default"`. Default: `"default"`
71
- - Children render in the actions slot (right side)
71
+ - Children render in the actions slot (right side). Badges, buttons, and icons are all valid — but the item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.
72
72
 
73
73
  **progress** — Horizontal progress bar.
74
74
  - `value` (number, required, 0 to max)
@@ -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"`. Taps write to `inputs[name]`
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
 
@@ -165,7 +166,7 @@ Bound to buttons via `on.press`:
165
166
  | `open_mini_app` | `target` (URL) | Open as Farcaster mini app |
166
167
  | `view_cast` | `hash` (string) | Navigate to a cast |
167
168
  | `view_profile` | `fid` (number) | Navigate to a profile |
168
- | `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer |
169
+ | `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer. Put URLs in `embeds`, not `text` |
169
170
  | `view_token` | `token` (CAIP-19) | View token in wallet |
170
171
  | `send_token` | `token`, `amount?`, `recipientFid?`, `recipientAddress?` | Send token flow |
171
172
  | `swap_token` | `sellToken?`, `buyToken?` | Swap token flow |
@@ -174,6 +175,8 @@ Bound to buttons via `on.press`:
174
175
 
175
176
  `arrow-right`, `arrow-left`, `external-link`, `chevron-right`, `check`, `x`, `alert-triangle`, `info`, `clock`, `heart`, `message-circle`, `repeat`, `share`, `user`, `users`, `star`, `trophy`, `zap`, `flame`, `gift`, `image`, `play`, `pause`, `wallet`, `coins`, `plus`, `minus`, `refresh-cw`, `bookmark`, `thumbs-up`, `thumbs-down`, `trending-up`, `trending-down`
176
177
 
178
+ `chevron-right`, `arrow-right`, and `external-link` are navigation/disclosure affordances — only use them when the surrounding element actually navigates (e.g. a button bound to `open_url` or `open_snap`). Never place them inside an `item`'s actions slot; `item` is not interactive.
179
+
177
180
  ## Color Palette
178
181
 
179
182
  `gray`, `blue`, `red`, `amber`, `green`, `teal`, `purple`, `pink`
@@ -194,6 +197,26 @@ import { withTursoServerless, createInMemoryDataStore } from "@farcaster/snap-tu
194
197
  - `@farcaster/snap-hono` — Hono adapter (`registerSnapHandler`)
195
198
  - `@farcaster/snap-turso` — `withTursoServerless`, `DataStore` / `DataStoreValue`, in-memory and Turso helpers
196
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
+
197
220
  ## Full Documentation
198
221
 
199
222
  https://docs.farcaster.xyz/snap
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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) => selectedSet.has(`${r},${c}`);
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 = interactive && selectedSet.size > 0
108
+ const selectionLabel = isSelectable && selectedSet.size > 0
103
109
  ? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
104
110
  : null;
105
111
 
@@ -59,6 +59,7 @@ export function SnapCard({
59
59
  validationErrorFallback,
60
60
  actionError,
61
61
  plain = false,
62
+ loadingOverlay,
62
63
  }: {
63
64
  snap: SnapPage;
64
65
  handlers: SnapActionHandlers;
@@ -73,6 +74,8 @@ export function SnapCard({
73
74
  actionError?: string | null;
74
75
  /** When true, renders without card frame (no border, background, or padding). */
75
76
  plain?: boolean;
77
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
78
+ loadingOverlay?: ReactNode;
76
79
  }) {
77
80
  if (snap.version === SPEC_VERSION_2) {
78
81
  return (
@@ -87,6 +90,7 @@ export function SnapCard({
87
90
  validationErrorFallback={validationErrorFallback}
88
91
  actionError={actionError}
89
92
  plain={plain}
93
+ loadingOverlay={loadingOverlay}
90
94
  />
91
95
  );
92
96
  }
@@ -100,6 +104,7 @@ export function SnapCard({
100
104
  maxWidth={maxWidth}
101
105
  actionError={actionError}
102
106
  plain={plain}
107
+ loadingOverlay={loadingOverlay}
103
108
  />
104
109
  );
105
110
  }
@@ -8,6 +8,7 @@ import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex";
8
8
  import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css";
9
9
  import {
10
10
  type CSSProperties,
11
+ type ReactNode,
11
12
  useCallback,
12
13
  useEffect,
13
14
  useMemo,
@@ -111,7 +112,7 @@ function ConfettiOverlay() {
111
112
  );
112
113
  }
113
114
 
114
- function SnapLoadingOverlay({
115
+ export function SnapLoadingOverlay({
115
116
  appearance,
116
117
  accentHex,
117
118
  active,
@@ -197,11 +198,17 @@ export function SnapViewCore({
197
198
  handlers,
198
199
  loading = false,
199
200
  appearance = "dark",
201
+ loadingOverlay,
200
202
  }: {
201
203
  snap: SnapPage;
202
204
  handlers: SnapActionHandlers;
203
205
  loading?: boolean;
204
206
  appearance?: "light" | "dark";
207
+ /**
208
+ * Custom content rendered while `loading` is true. When `undefined` (default)
209
+ * the built-in spinner + backdrop is used. Pass `null` to render nothing.
210
+ */
211
+ loadingOverlay?: ReactNode;
205
212
  }) {
206
213
  const spec = snap.ui;
207
214
  const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
@@ -232,7 +239,11 @@ export function SnapViewCore({
232
239
  setPageKey((k) => k + 1);
233
240
  }, [spec]);
234
241
 
235
- 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]);
236
247
 
237
248
  const accentName = snap.theme?.accent ?? "purple";
238
249
 
@@ -314,12 +325,16 @@ export function SnapViewCore({
314
325
 
315
326
  return (
316
327
  <div style={{ position: "relative", width: "100%" }}>
317
- {showConfetti && <ConfettiOverlay />}
318
- <SnapLoadingOverlay
319
- appearance={appearance}
320
- accentHex={accentHex}
321
- active={loading}
322
- />
328
+ {showConfetti && <ConfettiOverlay key={confettiKey} />}
329
+ {loadingOverlay === undefined ? (
330
+ <SnapLoadingOverlay
331
+ appearance={appearance}
332
+ accentHex={accentHex}
333
+ active={loading}
334
+ />
335
+ ) : loading ? (
336
+ <>{loadingOverlay}</>
337
+ ) : null}
323
338
 
324
339
  <div style={previewSurfaceStyle}>
325
340
  <SnapPreviewAccentProvider
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
4
- import { SnapViewCore } from "../snap-view-core";
3
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
4
+ import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
5
+ import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
5
6
  import type { SnapPage, SnapActionHandlers } from "../index";
6
7
 
7
8
  const SNAP_MAX_HEIGHT = 500;
@@ -11,11 +12,14 @@ export function SnapViewV1({
11
12
  handlers,
12
13
  loading = false,
13
14
  appearance = "dark",
15
+ loadingOverlay,
14
16
  }: {
15
17
  snap: SnapPage;
16
18
  handlers: SnapActionHandlers;
17
19
  loading?: boolean;
18
20
  appearance?: "light" | "dark";
21
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
22
+ loadingOverlay?: ReactNode;
19
23
  }) {
20
24
  return (
21
25
  <SnapViewCore
@@ -23,6 +27,7 @@ export function SnapViewV1({
23
27
  handlers={handlers}
24
28
  loading={loading}
25
29
  appearance={appearance}
30
+ loadingOverlay={loadingOverlay}
26
31
  />
27
32
  );
28
33
  }
@@ -35,6 +40,7 @@ export function SnapCardV1({
35
40
  maxWidth = 480,
36
41
  actionError,
37
42
  plain = false,
43
+ loadingOverlay,
38
44
  }: {
39
45
  snap: SnapPage;
40
46
  handlers: SnapActionHandlers;
@@ -43,6 +49,8 @@ export function SnapCardV1({
43
49
  maxWidth?: number;
44
50
  actionError?: string | null;
45
51
  plain?: boolean;
52
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
53
+ loadingOverlay?: ReactNode;
46
54
  }) {
47
55
  const isDark = appearance === "dark";
48
56
  const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
@@ -86,76 +94,97 @@ export function SnapCardV1({
86
94
 
87
95
  const isClipped = isExpandable && !isExpanded;
88
96
 
97
+ const accentHex = useMemo(
98
+ () => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance),
99
+ [snap.theme?.accent, appearance],
100
+ );
101
+
89
102
  return (
90
103
  <div
91
104
  style={{
92
105
  position: "relative",
93
106
  width: "100%",
94
107
  maxWidth,
95
- overflow: "hidden",
96
- ...(plain ? {} : {
97
- borderRadius: 16,
98
- border: `1px solid ${borderColor}`,
99
- backgroundColor: surfaceBg,
100
- }),
101
108
  }}
102
109
  >
103
110
  <div
104
- style={
105
- isClipped
106
- ? {
107
- maxHeight: SNAP_MAX_HEIGHT,
108
- overflow: "hidden",
109
- }
110
- : undefined
111
- }
111
+ style={{
112
+ position: "relative",
113
+ overflow: "hidden",
114
+ ...(plain ? {} : {
115
+ borderRadius: 16,
116
+ border: `1px solid ${borderColor}`,
117
+ backgroundColor: surfaceBg,
118
+ }),
119
+ }}
112
120
  >
113
- <div ref={contentRef} style={plain ? undefined : { padding: 16 }}>
114
- <SnapViewV1
115
- snap={snap}
116
- handlers={handlers}
117
- loading={loading}
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
118
143
  appearance={appearance}
144
+ accentHex={accentHex}
145
+ active={loading}
119
146
  />
120
- </div>
147
+ ) : loading ? (
148
+ <>{loadingOverlay}</>
149
+ ) : null}
121
150
  </div>
122
151
  {isExpandable ? (
123
- <div
152
+ <button
153
+ type="button"
154
+ aria-expanded={isExpanded}
155
+ onClick={() => setIsExpanded((value) => !value)}
124
156
  style={{
125
- display: "flex",
126
- justifyContent: "center",
127
- padding: plain ? "8px 0 0" : "10px 16px 12px",
128
- ...(plain
129
- ? {}
130
- : { borderTop: `1px solid ${borderColor}` }),
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)";
131
184
  }}
132
185
  >
133
- <button
134
- type="button"
135
- aria-expanded={isExpanded}
136
- onClick={() => setIsExpanded((value) => !value)}
137
- style={{
138
- appearance: "none",
139
- border: "none",
140
- borderRadius: 9999,
141
- backgroundColor: toggleBg,
142
- color: toggleText,
143
- padding: "6px 10px",
144
- fontSize: 13,
145
- lineHeight: "18px",
146
- fontWeight: 600,
147
- cursor: "pointer",
148
- }}
149
- onMouseEnter={(event) => {
150
- event.currentTarget.style.backgroundColor = toggleBgHover;
151
- }}
152
- onMouseLeave={(event) => {
153
- event.currentTarget.style.backgroundColor = toggleBg;
154
- }}
155
- >
156
- {isExpanded ? "Show less" : "Show more"}
157
- </button>
158
- </div>
186
+ {isExpanded ? "Show less" : "Show more"}
187
+ </button>
159
188
  ) : null}
160
189
  {actionError && (
161
190
  <div