@farcaster/snap 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/colors.d.ts +4 -4
  2. package/dist/colors.js +20 -20
  3. package/dist/constants.d.ts +17 -1
  4. package/dist/constants.js +19 -1
  5. package/dist/index.d.ts +4 -6
  6. package/dist/index.js +2 -4
  7. package/dist/react/accent-context.d.ts +3 -1
  8. package/dist/react/accent-context.js +7 -4
  9. package/dist/react/catalog-renderer.js +4 -0
  10. package/dist/react/components/action-button.d.ts +2 -1
  11. package/dist/react/components/action-button.js +32 -13
  12. package/dist/react/components/badge.js +8 -8
  13. package/dist/react/components/bar-chart.d.ts +5 -0
  14. package/dist/react/components/bar-chart.js +26 -0
  15. package/dist/react/components/cell-grid.d.ts +5 -0
  16. package/dist/react/components/cell-grid.js +87 -0
  17. package/dist/react/components/icon.js +4 -10
  18. package/dist/react/components/input.js +12 -6
  19. package/dist/react/components/item-group.js +3 -1
  20. package/dist/react/components/item.d.ts +3 -3
  21. package/dist/react/components/item.js +4 -3
  22. package/dist/react/components/progress.js +3 -3
  23. package/dist/react/components/separator.js +3 -1
  24. package/dist/react/components/slider.js +15 -10
  25. package/dist/react/components/switch.js +10 -12
  26. package/dist/react/components/text.js +6 -14
  27. package/dist/react/components/toggle-group.js +20 -6
  28. package/dist/react/hooks/use-snap-colors.d.ts +38 -0
  29. package/dist/react/hooks/use-snap-colors.js +81 -0
  30. package/dist/react/index.d.ts +13 -1
  31. package/dist/react/index.js +9 -188
  32. package/dist/react/snap-view-core.d.ts +11 -0
  33. package/dist/react/snap-view-core.js +224 -0
  34. package/dist/react/v1/snap-view.d.ts +16 -0
  35. package/dist/react/v1/snap-view.js +90 -0
  36. package/dist/react/v2/snap-view.d.ts +23 -0
  37. package/dist/react/v2/snap-view.js +91 -0
  38. package/dist/react-native/catalog-renderer.d.ts +5 -0
  39. package/dist/react-native/catalog-renderer.js +40 -0
  40. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  41. package/dist/react-native/components/snap-action-button.js +69 -0
  42. package/dist/react-native/components/snap-badge.d.ts +2 -0
  43. package/dist/react-native/components/snap-badge.js +41 -0
  44. package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
  45. package/dist/react-native/components/snap-bar-chart.js +39 -0
  46. package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
  47. package/dist/react-native/components/snap-cell-grid.js +94 -0
  48. package/dist/react-native/components/snap-icon.d.ts +5 -0
  49. package/dist/react-native/components/snap-icon.js +56 -0
  50. package/dist/react-native/components/snap-image.d.ts +2 -0
  51. package/dist/react-native/components/snap-image.js +23 -0
  52. package/dist/react-native/components/snap-input.d.ts +2 -0
  53. package/dist/react-native/components/snap-input.js +37 -0
  54. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  55. package/dist/react-native/components/snap-item-group.js +23 -0
  56. package/dist/react-native/components/snap-item.d.ts +5 -0
  57. package/dist/react-native/components/snap-item.js +42 -0
  58. package/dist/react-native/components/snap-progress.d.ts +2 -0
  59. package/dist/react-native/components/snap-progress.js +26 -0
  60. package/dist/react-native/components/snap-separator.d.ts +2 -0
  61. package/dist/react-native/components/snap-separator.js +23 -0
  62. package/dist/react-native/components/snap-slider.d.ts +2 -0
  63. package/dist/react-native/components/snap-slider.js +43 -0
  64. package/dist/react-native/components/snap-stack.d.ts +5 -0
  65. package/dist/react-native/components/snap-stack.js +49 -0
  66. package/dist/react-native/components/snap-switch.d.ts +2 -0
  67. package/dist/react-native/components/snap-switch.js +31 -0
  68. package/dist/react-native/components/snap-text.d.ts +2 -0
  69. package/dist/react-native/components/snap-text.js +35 -0
  70. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  71. package/dist/react-native/components/snap-toggle-group.js +99 -0
  72. package/dist/react-native/confetti-overlay.d.ts +1 -0
  73. package/dist/react-native/confetti-overlay.js +106 -0
  74. package/dist/react-native/index.d.ts +28 -0
  75. package/dist/react-native/index.js +15 -0
  76. package/dist/react-native/snap-view-core.d.ts +11 -0
  77. package/dist/react-native/snap-view-core.js +153 -0
  78. package/dist/react-native/theme.d.ts +27 -0
  79. package/dist/react-native/theme.js +43 -0
  80. package/dist/react-native/types.d.ts +42 -0
  81. package/dist/react-native/types.js +1 -0
  82. package/dist/react-native/use-snap-palette.d.ts +13 -0
  83. package/dist/react-native/use-snap-palette.js +48 -0
  84. package/dist/react-native/v1/snap-view.d.ts +24 -0
  85. package/dist/react-native/v1/snap-view.js +96 -0
  86. package/dist/react-native/v2/snap-view.d.ts +33 -0
  87. package/dist/react-native/v2/snap-view.js +114 -0
  88. package/dist/schemas.d.ts +100 -13
  89. package/dist/schemas.js +28 -10
  90. package/dist/server/parseRequest.d.ts +10 -0
  91. package/dist/server/parseRequest.js +48 -7
  92. package/dist/server/verify.d.ts +1 -0
  93. package/dist/server/verify.js +1 -0
  94. package/dist/ui/badge.d.ts +7 -2
  95. package/dist/ui/badge.js +2 -0
  96. package/dist/ui/bar-chart.d.ts +30 -0
  97. package/dist/ui/bar-chart.js +30 -0
  98. package/dist/ui/button.d.ts +4 -6
  99. package/dist/ui/button.js +1 -1
  100. package/dist/ui/catalog.d.ts +90 -16
  101. package/dist/ui/catalog.js +17 -3
  102. package/dist/ui/cell-grid.d.ts +34 -0
  103. package/dist/ui/cell-grid.js +39 -0
  104. package/dist/ui/icon.d.ts +2 -2
  105. package/dist/ui/image.d.ts +1 -2
  106. package/dist/ui/image.js +1 -1
  107. package/dist/ui/index.d.ts +4 -0
  108. package/dist/ui/index.js +2 -0
  109. package/dist/ui/item.d.ts +1 -3
  110. package/dist/ui/item.js +1 -1
  111. package/dist/ui/schema.d.ts +6 -2
  112. package/dist/ui/schema.js +2 -2
  113. package/dist/ui/slider.d.ts +1 -0
  114. package/dist/ui/slider.js +2 -0
  115. package/dist/ui/text.d.ts +2 -4
  116. package/dist/ui/text.js +2 -2
  117. package/dist/validator.d.ts +3 -2
  118. package/dist/validator.js +198 -2
  119. package/llms.txt +199 -0
  120. package/package.json +9 -3
  121. package/src/colors.ts +20 -20
  122. package/src/constants.ts +23 -1
  123. package/src/index.ts +16 -13
  124. package/src/react/accent-context.tsx +13 -6
  125. package/src/react/catalog-renderer.tsx +4 -0
  126. package/src/react/components/action-button.tsx +47 -20
  127. package/src/react/components/badge.tsx +14 -18
  128. package/src/react/components/bar-chart.tsx +69 -0
  129. package/src/react/components/cell-grid.tsx +128 -0
  130. package/src/react/components/icon.tsx +5 -18
  131. package/src/react/components/input.tsx +20 -9
  132. package/src/react/components/item-group.tsx +4 -1
  133. package/src/react/components/item.tsx +13 -10
  134. package/src/react/components/progress.tsx +12 -7
  135. package/src/react/components/separator.tsx +8 -1
  136. package/src/react/components/slider.tsx +28 -15
  137. package/src/react/components/switch.tsx +12 -16
  138. package/src/react/components/text.tsx +14 -23
  139. package/src/react/components/toggle-group.tsx +26 -9
  140. package/src/react/hooks/use-snap-colors.ts +128 -0
  141. package/src/react/index.tsx +49 -265
  142. package/src/react/snap-view-core.tsx +340 -0
  143. package/src/react/v1/snap-view.tsx +176 -0
  144. package/src/react/v2/snap-view.tsx +199 -0
  145. package/src/react-native/catalog-renderer.tsx +41 -0
  146. package/src/react-native/components/snap-action-button.tsx +96 -0
  147. package/src/react-native/components/snap-badge.tsx +60 -0
  148. package/src/react-native/components/snap-bar-chart.tsx +73 -0
  149. package/src/react-native/components/snap-cell-grid.tsx +150 -0
  150. package/src/react-native/components/snap-icon.tsx +102 -0
  151. package/src/react-native/components/snap-image.tsx +37 -0
  152. package/src/react-native/components/snap-input.tsx +58 -0
  153. package/src/react-native/components/snap-item-group.tsx +43 -0
  154. package/src/react-native/components/snap-item.tsx +66 -0
  155. package/src/react-native/components/snap-progress.tsx +40 -0
  156. package/src/react-native/components/snap-separator.tsx +32 -0
  157. package/src/react-native/components/snap-slider.tsx +85 -0
  158. package/src/react-native/components/snap-stack.tsx +66 -0
  159. package/src/react-native/components/snap-switch.tsx +46 -0
  160. package/src/react-native/components/snap-text.tsx +51 -0
  161. package/src/react-native/components/snap-toggle-group.tsx +127 -0
  162. package/src/react-native/confetti-overlay.tsx +134 -0
  163. package/src/react-native/index.tsx +83 -0
  164. package/src/react-native/snap-view-core.tsx +209 -0
  165. package/src/react-native/theme.tsx +85 -0
  166. package/src/react-native/types.ts +38 -0
  167. package/src/react-native/use-snap-palette.ts +64 -0
  168. package/src/react-native/v1/snap-view.tsx +229 -0
  169. package/src/react-native/v2/snap-view.tsx +283 -0
  170. package/src/schemas.ts +68 -17
  171. package/src/server/parseRequest.ts +68 -9
  172. package/src/server/verify.ts +2 -0
  173. package/src/ui/README.md +8 -8
  174. package/src/ui/badge.ts +2 -0
  175. package/src/ui/bar-chart.ts +38 -0
  176. package/src/ui/button.ts +1 -1
  177. package/src/ui/catalog.ts +19 -3
  178. package/src/ui/cell-grid.ts +49 -0
  179. package/src/ui/image.ts +1 -1
  180. package/src/ui/index.ts +6 -0
  181. package/src/ui/item.ts +1 -1
  182. package/src/ui/schema.ts +2 -2
  183. package/src/ui/slider.ts +2 -0
  184. package/src/ui/text.ts +2 -2
  185. package/src/validator.ts +246 -2
  186. package/dist/dataStore.d.ts +0 -12
  187. package/dist/dataStore.js +0 -35
  188. package/dist/middleware.d.ts +0 -3
  189. package/dist/middleware.js +0 -3
  190. package/dist/react/hooks/use-snap-accent.d.ts +0 -13
  191. package/dist/react/hooks/use-snap-accent.js +0 -32
  192. package/src/dataStore.ts +0 -62
  193. package/src/middleware.ts +0 -7
  194. package/src/react/hooks/use-snap-accent.ts +0 -45
@@ -0,0 +1,16 @@
1
+ import type { SnapPage, SnapActionHandlers } from "../index.js";
2
+ export declare function SnapViewV1({ snap, handlers, loading, appearance, }: {
3
+ snap: SnapPage;
4
+ handlers: SnapActionHandlers;
5
+ loading?: boolean;
6
+ appearance?: "light" | "dark";
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, plain, }: {
9
+ snap: SnapPage;
10
+ handlers: SnapActionHandlers;
11
+ loading?: boolean;
12
+ appearance?: "light" | "dark";
13
+ maxWidth?: number;
14
+ actionError?: string | null;
15
+ plain?: boolean;
16
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,90 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { SnapViewCore } from "../snap-view-core.js";
5
+ const SNAP_MAX_HEIGHT = 500;
6
+ export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", }) {
7
+ return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
8
+ }
9
+ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, actionError, plain = false, }) {
10
+ const isDark = appearance === "dark";
11
+ const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
12
+ const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
13
+ const toggleBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
14
+ const toggleBgHover = isDark
15
+ ? "rgba(255,255,255,0.1)"
16
+ : "rgba(0,0,0,0.08)";
17
+ const toggleText = isDark ? "rgba(255,255,255,0.82)" : "rgba(0,0,0,0.72)";
18
+ const contentRef = useRef(null);
19
+ const [isExpandable, setIsExpandable] = useState(false);
20
+ const [isExpanded, setIsExpanded] = useState(false);
21
+ useEffect(() => {
22
+ setIsExpanded(false);
23
+ }, [snap]);
24
+ useEffect(() => {
25
+ const node = contentRef.current;
26
+ if (!node)
27
+ return;
28
+ const measure = () => {
29
+ setIsExpandable(node.scrollHeight > SNAP_MAX_HEIGHT + 1);
30
+ };
31
+ measure();
32
+ if (typeof ResizeObserver === "undefined")
33
+ return;
34
+ const observer = new ResizeObserver(() => {
35
+ measure();
36
+ });
37
+ observer.observe(node);
38
+ return () => observer.disconnect();
39
+ }, [snap, plain]);
40
+ useEffect(() => {
41
+ if (!isExpandable) {
42
+ setIsExpanded(false);
43
+ }
44
+ }, [isExpandable]);
45
+ const isClipped = isExpandable && !isExpanded;
46
+ return (_jsxs("div", { style: {
47
+ position: "relative",
48
+ width: "100%",
49
+ maxWidth,
50
+ overflow: "hidden",
51
+ ...(plain ? {} : {
52
+ borderRadius: 16,
53
+ border: `1px solid ${borderColor}`,
54
+ backgroundColor: surfaceBg,
55
+ }),
56
+ }, children: [_jsx("div", { style: isClipped
57
+ ? {
58
+ maxHeight: SNAP_MAX_HEIGHT,
59
+ overflow: "hidden",
60
+ }
61
+ : undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }) }) }), isExpandable ? (_jsx("div", { style: {
62
+ display: "flex",
63
+ justifyContent: "center",
64
+ padding: plain ? "8px 0 0" : "10px 16px 12px",
65
+ ...(plain
66
+ ? {}
67
+ : { borderTop: `1px solid ${borderColor}` }),
68
+ }, children: _jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
69
+ appearance: "none",
70
+ border: "none",
71
+ borderRadius: 9999,
72
+ backgroundColor: toggleBg,
73
+ color: toggleText,
74
+ padding: "6px 10px",
75
+ fontSize: 13,
76
+ lineHeight: "18px",
77
+ fontWeight: 600,
78
+ cursor: "pointer",
79
+ }, onMouseEnter: (event) => {
80
+ event.currentTarget.style.backgroundColor = toggleBgHover;
81
+ }, onMouseLeave: (event) => {
82
+ event.currentTarget.style.backgroundColor = toggleBg;
83
+ }, children: isExpanded ? "Show less" : "Show more" }) })) : null, actionError && (_jsx("div", { style: {
84
+ padding: "8px 12px",
85
+ fontSize: 13,
86
+ color: appearance === "dark"
87
+ ? "rgba(255,100,100,0.9)"
88
+ : "rgba(200,0,0,0.8)",
89
+ }, children: actionError }))] }));
90
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ValidationResult } from "../../validator.js";
3
+ import type { SnapPage, SnapActionHandlers } from "../index.js";
4
+ export declare function SnapViewV2({ snap, handlers, loading, appearance, onValidationError, validationErrorFallback, }: {
5
+ snap: SnapPage;
6
+ handlers: SnapActionHandlers;
7
+ loading?: boolean;
8
+ appearance?: "light" | "dark";
9
+ onValidationError?: (result: ValidationResult) => void;
10
+ validationErrorFallback?: ReactNode;
11
+ }): import("react/jsx-runtime").JSX.Element | null;
12
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
13
+ snap: SnapPage;
14
+ handlers: SnapActionHandlers;
15
+ loading?: boolean;
16
+ appearance?: "light" | "dark";
17
+ maxWidth?: number;
18
+ showOverflowWarning?: boolean;
19
+ onValidationError?: (result: ValidationResult) => void;
20
+ validationErrorFallback?: ReactNode;
21
+ actionError?: string | null;
22
+ plain?: boolean;
23
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo } from "react";
4
+ import { validateSnapResponse } from "../../validator.js";
5
+ import { SnapViewCore } from "../snap-view-core.js";
6
+ const SNAP_MAX_HEIGHT = 500;
7
+ const SNAP_WARNING_HEIGHT = 700;
8
+ // ─── Default validation error fallback ────────────────
9
+ function SnapValidationFallback({ appearance, message, }) {
10
+ const isDark = appearance === "dark";
11
+ return (_jsx("div", { style: {
12
+ width: "100%",
13
+ padding: 16,
14
+ display: "flex",
15
+ alignItems: "center",
16
+ justifyContent: "center",
17
+ color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.4)",
18
+ fontSize: 14,
19
+ }, children: _jsx("span", { children: message ? `Unable to render snap: ${message}` : "Unable to render snap" }) }));
20
+ }
21
+ // ─── SnapViewV2 ──────────────────────────────────────
22
+ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", onValidationError, validationErrorFallback, }) {
23
+ const validation = useMemo(() => validateSnapResponse(snap), [snap]);
24
+ const valid = validation.valid;
25
+ const validationMessage = validation.issues[0]?.message;
26
+ useEffect(() => {
27
+ if (!valid) {
28
+ if (onValidationError) {
29
+ onValidationError(validation);
30
+ }
31
+ else {
32
+ // eslint-disable-next-line no-console
33
+ console.warn("[Snap] validation issues:", validation.issues);
34
+ }
35
+ }
36
+ }, [valid, validation, onValidationError]);
37
+ if (!valid) {
38
+ if (validationErrorFallback === null)
39
+ return null;
40
+ return _jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { appearance: appearance, message: validationMessage }) });
41
+ }
42
+ return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
43
+ }
44
+ // ─── SnapCardV2 ──────────────────────────────────────
45
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
46
+ const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
47
+ const isDark = appearance === "dark";
48
+ const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
49
+ const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
50
+ const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
51
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
52
+ position: "relative",
53
+ width: "100%",
54
+ maxWidth,
55
+ maxHeight,
56
+ overflow: "hidden",
57
+ ...(plain ? {} : {
58
+ borderRadius: 16,
59
+ border: `1px solid ${borderColor}`,
60
+ backgroundColor: surfaceBg,
61
+ }),
62
+ }, children: [_jsx("div", { style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }), showOverflowWarning && (_jsxs("div", { style: {
63
+ position: "absolute",
64
+ top: SNAP_MAX_HEIGHT,
65
+ left: 0,
66
+ right: 0,
67
+ bottom: 0,
68
+ pointerEvents: "none",
69
+ zIndex: 10,
70
+ }, children: [_jsx("div", { style: { borderTop: "1px dashed rgba(255,100,100,0.6)", position: "relative" }, children: _jsxs("span", { style: {
71
+ position: "absolute",
72
+ top: -10,
73
+ right: 0,
74
+ fontSize: 10,
75
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
76
+ color: "rgba(255,100,100,0.7)",
77
+ background: bg,
78
+ padding: "1px 4px",
79
+ borderRadius: 3,
80
+ }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx("div", { style: {
81
+ height: "100%",
82
+ background: "repeating-linear-gradient(-45deg, transparent, transparent 8px, rgba(255,100,100,0.06) 8px, rgba(255,100,100,0.06) 16px)",
83
+ } })] }))] }), actionError && (_jsx("div", { style: {
84
+ maxWidth,
85
+ padding: "8px 12px",
86
+ fontSize: 13,
87
+ color: appearance === "dark"
88
+ ? "rgba(255,100,100,0.9)"
89
+ : "rgba(200,0,0,0.8)",
90
+ }, children: actionError }))] }));
91
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Maps snap json-render catalog types to React Native primitives.
3
+ * Keys match the snap wire-format `type` strings exactly (snake_case).
4
+ */
5
+ export declare const SnapCatalogView: ComponentType<import("@json-render/react-native").CreateRendererProps>;
@@ -0,0 +1,40 @@
1
+ import { createRenderer } from "@json-render/react-native";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapActionButton } from "./components/snap-action-button.js";
4
+ import { SnapBadge } from "./components/snap-badge.js";
5
+ import { SnapIcon } from "./components/snap-icon.js";
6
+ import { SnapImage } from "./components/snap-image.js";
7
+ import { SnapInput } from "./components/snap-input.js";
8
+ import { SnapItem } from "./components/snap-item.js";
9
+ import { SnapItemGroup } from "./components/snap-item-group.js";
10
+ import { SnapProgress } from "./components/snap-progress.js";
11
+ import { SnapSeparator } from "./components/snap-separator.js";
12
+ import { SnapSlider } from "./components/snap-slider.js";
13
+ import { SnapStack } from "./components/snap-stack.js";
14
+ import { SnapSwitch } from "./components/snap-switch.js";
15
+ import { SnapText } from "./components/snap-text.js";
16
+ import { SnapToggleGroup } from "./components/snap-toggle-group.js";
17
+ import { SnapBarChart } from "./components/snap-bar-chart.js";
18
+ import { SnapCellGrid } from "./components/snap-cell-grid.js";
19
+ /**
20
+ * Maps snap json-render catalog types to React Native primitives.
21
+ * Keys match the snap wire-format `type` strings exactly (snake_case).
22
+ */
23
+ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
24
+ badge: SnapBadge,
25
+ button: SnapActionButton,
26
+ icon: SnapIcon,
27
+ image: SnapImage,
28
+ input: SnapInput,
29
+ item: SnapItem,
30
+ item_group: SnapItemGroup,
31
+ progress: SnapProgress,
32
+ separator: SnapSeparator,
33
+ slider: SnapSlider,
34
+ stack: SnapStack,
35
+ switch: SnapSwitch,
36
+ text: SnapText,
37
+ toggle_group: SnapToggleGroup,
38
+ bar_chart: SnapBarChart,
39
+ cell_grid: SnapCellGrid,
40
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapActionButton({ element, emit, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { ExternalLink } from "lucide-react-native";
4
+ import { useSnapPalette } from "../use-snap-palette.js";
5
+ import { useSnapTheme } from "../theme.js";
6
+ import { ICON_MAP } from "./snap-icon.js";
7
+ function isExternalLinkAction(on) {
8
+ if (!on)
9
+ return false;
10
+ const press = on.press;
11
+ if (!press)
12
+ return false;
13
+ return press.action === "open_url";
14
+ }
15
+ export function SnapActionButton({ element, emit, }) {
16
+ const { accentHex } = useSnapPalette();
17
+ const { colors } = useSnapTheme();
18
+ const { props } = element;
19
+ const label = String(props.label ?? "Action");
20
+ const variant = String(props.variant ?? "secondary");
21
+ const isPrimary = variant === "primary";
22
+ const iconName = props.icon ? String(props.icon) : undefined;
23
+ const textColor = isPrimary ? "#fff" : colors.text;
24
+ const iconColor = isPrimary ? "#fff" : colors.text;
25
+ const on = element.on;
26
+ const showExternalIcon = isExternalLinkAction(on);
27
+ return (_jsx(View, { style: styles.outer, children: _jsxs(Pressable, { style: ({ pressed }) => [
28
+ styles.btn,
29
+ isPrimary ? styles.btnDefault : styles.btnOther,
30
+ isPrimary
31
+ ? { backgroundColor: pressed ? accentHex + "DD" : accentHex }
32
+ : { backgroundColor: pressed ? colors.mutedHover : colors.muted },
33
+ pressed && styles.pressed,
34
+ ], onPress: () => {
35
+ void (async () => {
36
+ try {
37
+ await emit("press");
38
+ }
39
+ catch (err) {
40
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
41
+ console.error("[snap] action failed", err);
42
+ }
43
+ }
44
+ })();
45
+ }, children: [iconName && ICON_MAP[iconName]
46
+ ? (() => {
47
+ const I = ICON_MAP[iconName];
48
+ return _jsx(I, { size: 16, color: iconColor });
49
+ })()
50
+ : null, _jsx(Text, { style: { color: textColor, fontSize: 14, lineHeight: 18, fontWeight: "600" }, children: label }), showExternalIcon ? (_jsx(ExternalLink, { size: 14, color: iconColor, style: { opacity: 0.6 } })) : null] }) }));
51
+ }
52
+ const styles = StyleSheet.create({
53
+ outer: { minWidth: 0 },
54
+ btn: {
55
+ paddingHorizontal: 16,
56
+ borderRadius: 10,
57
+ alignItems: "center",
58
+ justifyContent: "center",
59
+ flexDirection: "row",
60
+ gap: 8,
61
+ },
62
+ btnDefault: {
63
+ paddingVertical: 10,
64
+ },
65
+ btnOther: {
66
+ paddingVertical: 8,
67
+ },
68
+ pressed: { opacity: 0.88 },
69
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapBadge({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette.js";
4
+ import { ICON_MAP } from "./snap-icon.js";
5
+ export function SnapBadge({ element: { props }, }) {
6
+ const { accentHex, hex } = useSnapPalette();
7
+ const label = String(props.label ?? "");
8
+ const variant = String(props.variant ?? "default");
9
+ const color = props.color ? String(props.color) : undefined;
10
+ const iconName = props.icon ? String(props.icon) : undefined;
11
+ const isAccent = !color || color === "accent";
12
+ const resolvedColor = isAccent ? accentHex : hex(color);
13
+ const isFilled = variant !== "outline";
14
+ const Icon = iconName ? ICON_MAP[iconName] : undefined;
15
+ return (_jsxs(View, { style: [
16
+ styles.badge,
17
+ isFilled
18
+ ? { backgroundColor: resolvedColor + "20", borderColor: "transparent" }
19
+ : { borderColor: resolvedColor },
20
+ ], children: [Icon && (_jsx(Icon, { size: 12, color: resolvedColor })), _jsx(Text, { style: [
21
+ styles.label,
22
+ { color: resolvedColor },
23
+ ], children: label })] }));
24
+ }
25
+ const styles = StyleSheet.create({
26
+ badge: {
27
+ alignSelf: "flex-start",
28
+ flexDirection: "row",
29
+ alignItems: "center",
30
+ gap: 4,
31
+ paddingHorizontal: 8,
32
+ paddingVertical: 2,
33
+ borderRadius: 9999,
34
+ borderWidth: 1,
35
+ },
36
+ label: {
37
+ fontSize: 12,
38
+ lineHeight: 16,
39
+ fontWeight: "500",
40
+ },
41
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapBarChart({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette.js";
4
+ import { useSnapTheme } from "../theme.js";
5
+ export function SnapBarChart({ element: { props }, }) {
6
+ const { accentHex, hex } = useSnapPalette();
7
+ const { colors } = useSnapTheme();
8
+ const bars = Array.isArray(props.bars) ? props.bars : [];
9
+ const chartColor = String(props.color ?? "accent");
10
+ const maxVal = props.max != null
11
+ ? Number(props.max)
12
+ : Math.max(...bars.map((b) => Number(b.value ?? 0)), 1);
13
+ function barFill(bar) {
14
+ if (bar.color)
15
+ return hex(bar.color);
16
+ if (chartColor !== "accent")
17
+ return hex(chartColor);
18
+ return accentHex;
19
+ }
20
+ return (_jsx(View, { style: styles.wrap, children: bars.map((bar, i) => {
21
+ const value = Number(bar.value ?? 0);
22
+ const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
23
+ return (_jsxs(View, { style: styles.row, children: [_jsx(Text, { style: [styles.label, { color: colors.textSecondary }], numberOfLines: 1, children: String(bar.label ?? "") }), _jsx(View, { style: [styles.track, { backgroundColor: colors.muted }], children: _jsx(View, { style: [
24
+ styles.fill,
25
+ {
26
+ width: `${pct}%`,
27
+ backgroundColor: barFill(bar),
28
+ },
29
+ ] }) }), _jsx(Text, { style: [styles.value, { color: colors.textSecondary }], children: value })] }, i));
30
+ }) }));
31
+ }
32
+ const styles = StyleSheet.create({
33
+ wrap: { width: "100%", gap: 8 },
34
+ row: { flexDirection: "row", alignItems: "center", gap: 8 },
35
+ label: { width: 80, fontSize: 12, lineHeight: 16, textAlign: "right" },
36
+ track: { flex: 1, height: 10, borderRadius: 9999, overflow: "hidden" },
37
+ fill: { height: "100%", borderRadius: 9999 },
38
+ value: { width: 32, fontSize: 12, lineHeight: 16, fontVariant: ["tabular-nums"] },
39
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapCellGrid({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StyleSheet, Text, View, Pressable } from "react-native";
3
+ import { useStateStore } from "@json-render/react-native";
4
+ import { useSnapPalette } from "../use-snap-palette.js";
5
+ import { useSnapTheme } from "../theme.js";
6
+ import { POST_GRID_TAP_KEY } from "@farcaster/snap";
7
+ export function SnapCellGrid({ element: { props }, }) {
8
+ const { hex, appearance } = useSnapPalette();
9
+ const { colors } = useSnapTheme();
10
+ const { get, set } = useStateStore();
11
+ const cols = Number(props.cols ?? 2);
12
+ const rows = Number(props.rows ?? 2);
13
+ const cells = Array.isArray(props.cells) ? props.cells : [];
14
+ const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
15
+ const gap = String(props.gap ?? "sm");
16
+ const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
17
+ const gapPx = gapMap[gap] ?? 1;
18
+ const select = String(props.select ?? "off");
19
+ const interactive = select !== "off";
20
+ const isMultiple = select === "multiple";
21
+ const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
22
+ const tapPath = `/inputs/${name}`;
23
+ const tapRaw = get(tapPath);
24
+ const selectedSet = new Set();
25
+ if (typeof tapRaw === "string" && tapRaw.length > 0) {
26
+ for (const part of tapRaw.split("|")) {
27
+ if (part.includes(","))
28
+ selectedSet.add(part);
29
+ }
30
+ }
31
+ const isSelected = (r, c) => selectedSet.has(`${r},${c}`);
32
+ const handleTap = (r, c) => {
33
+ const key = `${r},${c}`;
34
+ if (isMultiple) {
35
+ const next = new Set(selectedSet);
36
+ if (next.has(key))
37
+ next.delete(key);
38
+ else
39
+ next.add(key);
40
+ set(tapPath, [...next].join("|"));
41
+ }
42
+ else {
43
+ set(tapPath, key);
44
+ }
45
+ };
46
+ const cellMap = new Map();
47
+ for (const c of cells) {
48
+ cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
49
+ color: c.color,
50
+ content: c.content != null ? String(c.content) : undefined,
51
+ });
52
+ }
53
+ const ringOuter = appearance === "dark" ? "#fff" : "#000";
54
+ const ringInner = appearance === "dark" ? "#000" : "#fff";
55
+ const rowEls = [];
56
+ for (let r = 0; r < rows; r++) {
57
+ const rowCells = [];
58
+ for (let c = 0; c < cols; c++) {
59
+ const cell = cellMap.get(`${r},${c}`);
60
+ const selected = interactive && isSelected(r, c);
61
+ const bg = cell?.color ? hex(cell.color) : "transparent";
62
+ const cellContent = cell?.content ? (_jsx(Text, { style: [styles.cellText, { color: colors.textPrimary }], children: cell.content })) : null;
63
+ // Two-tone ring: outer View with contrasting border, inner View with inverse border
64
+ const cellView = selected ? (_jsx(View, { style: [styles.cell, { height: rowHeight, borderWidth: 1, borderColor: ringOuter, borderRadius: 4 }], children: _jsx(View, { style: [
65
+ styles.innerCell,
66
+ { backgroundColor: bg, borderWidth: 1, borderColor: ringInner, borderRadius: 3 },
67
+ ], children: cellContent }) })) : (_jsx(View, { style: [styles.cell, { height: rowHeight, backgroundColor: bg }], children: cellContent }));
68
+ rowCells.push(interactive ? (_jsx(Pressable, { onPress: () => handleTap(r, c), style: styles.cellWrap, children: cellView }, `${r}-${c}`)) : (_jsx(View, { style: styles.cellWrap, children: cellView }, `${r}-${c}`)));
69
+ }
70
+ rowEls.push(_jsx(View, { style: [styles.gridRow, { gap: gapPx }], children: rowCells }, r));
71
+ }
72
+ const selectionLabel = interactive && selectedSet.size > 0
73
+ ? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
74
+ : null;
75
+ return (_jsxs(View, { style: [styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }], children: [rowEls, selectionLabel ? (_jsx(Text, { style: [styles.selectionText, { color: colors.textSecondary }], children: selectionLabel })) : null] }));
76
+ }
77
+ const styles = StyleSheet.create({
78
+ wrap: { width: "100%" },
79
+ gridRow: { flexDirection: "row" },
80
+ cellWrap: { flex: 1 },
81
+ cell: {
82
+ borderRadius: 4,
83
+ alignItems: "center",
84
+ justifyContent: "center",
85
+ },
86
+ innerCell: {
87
+ width: "100%",
88
+ height: "100%",
89
+ alignItems: "center",
90
+ justifyContent: "center",
91
+ },
92
+ cellText: { fontSize: 12, lineHeight: 16, fontWeight: "600" },
93
+ selectionText: { fontSize: 11, fontFamily: "monospace", marginTop: 6 },
94
+ });
@@ -0,0 +1,5 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { type LucideIcon } from "lucide-react-native";
3
+ declare const ICON_MAP: Record<string, LucideIcon>;
4
+ export declare function SnapIcon({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
5
+ export { ICON_MAP };
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette.js";
4
+ import { ArrowRight, ArrowLeft, ExternalLink, ChevronRight, Check, X, AlertTriangle, Info, Clock, Heart, MessageCircle, Repeat, Share, User, Users, Star, Trophy, Zap, Flame, Gift, ImageIcon, Play, Pause, Wallet, Coins, Plus, Minus, RefreshCw, Bookmark, ThumbsUp, ThumbsDown, TrendingUp, TrendingDown, } from "lucide-react-native";
5
+ const ICON_MAP = {
6
+ "arrow-right": ArrowRight,
7
+ "arrow-left": ArrowLeft,
8
+ "external-link": ExternalLink,
9
+ "chevron-right": ChevronRight,
10
+ check: Check,
11
+ x: X,
12
+ "alert-triangle": AlertTriangle,
13
+ info: Info,
14
+ clock: Clock,
15
+ heart: Heart,
16
+ "message-circle": MessageCircle,
17
+ repeat: Repeat,
18
+ share: Share,
19
+ user: User,
20
+ users: Users,
21
+ star: Star,
22
+ trophy: Trophy,
23
+ zap: Zap,
24
+ flame: Flame,
25
+ gift: Gift,
26
+ image: ImageIcon,
27
+ play: Play,
28
+ pause: Pause,
29
+ wallet: Wallet,
30
+ coins: Coins,
31
+ plus: Plus,
32
+ minus: Minus,
33
+ "refresh-cw": RefreshCw,
34
+ bookmark: Bookmark,
35
+ "thumbs-up": ThumbsUp,
36
+ "thumbs-down": ThumbsDown,
37
+ "trending-up": TrendingUp,
38
+ "trending-down": TrendingDown,
39
+ };
40
+ const SIZE_PX = {
41
+ sm: 16,
42
+ md: 20,
43
+ };
44
+ export function SnapIcon({ element: { props }, }) {
45
+ const { accentHex, hex } = useSnapPalette();
46
+ const name = String(props.name ?? "info");
47
+ const size = SIZE_PX[String(props.size ?? "md")] ?? 20;
48
+ const color = props.color ? String(props.color) : undefined;
49
+ const isAccent = !color || color === "accent";
50
+ const resolvedColor = isAccent ? accentHex : hex(color);
51
+ const Icon = ICON_MAP[name];
52
+ if (!Icon)
53
+ return null;
54
+ return (_jsx(View, { style: { alignItems: "center", justifyContent: "center" }, children: _jsx(Icon, { size: size, color: resolvedColor }) }));
55
+ }
56
+ export { ICON_MAP };
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapImage({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Image } from "expo-image";
3
+ import { StyleSheet, View } from "react-native";
4
+ function aspectToRatio(aspect) {
5
+ const [w, h] = aspect.split(":").map(Number);
6
+ if (!w || !h)
7
+ return 1;
8
+ return w / h;
9
+ }
10
+ export function SnapImage({ element: { props }, }) {
11
+ const url = String(props.url ?? "");
12
+ const alt = String(props.alt ?? "");
13
+ const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
14
+ return (_jsx(View, { style: [styles.frame, { aspectRatio: ratio }], children: _jsx(Image, { source: { uri: url }, style: StyleSheet.absoluteFill, contentFit: "cover", accessibilityLabel: alt || undefined }) }));
15
+ }
16
+ const styles = StyleSheet.create({
17
+ frame: {
18
+ width: "100%",
19
+ borderRadius: 8,
20
+ overflow: "hidden",
21
+ backgroundColor: "#f3f4f6",
22
+ },
23
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapInput({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;