@farcaster/snap 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +35 -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 +227 -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 +156 -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 +203 -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 +50 -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 +343 -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 +212 -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 +251 -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
@@ -2,22 +2,20 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { useStateStore } from "@json-render/react";
5
- import { Label } from "@neynar/ui/label";
6
5
  import { Switch } from "@neynar/ui/switch";
7
- import { useColorMode } from "@neynar/ui/color-mode";
8
- import { cn } from "@neynar/ui/utils";
9
- import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
6
+ import { Label } from "@neynar/ui/label";
7
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
10
8
  export function SnapSwitch({ element: { props }, }) {
11
- const id = useId();
12
9
  const { get, set } = useStateStore();
13
- const { mode } = useColorMode();
14
- const accentStyle = useSnapAccentScopeStyle();
10
+ const colors = useSnapColors();
15
11
  const name = String(props.name ?? "switch");
16
- const path = `/inputs/${name}`;
17
12
  const label = props.label ? String(props.label) : undefined;
18
- const fallback = Boolean(props.defaultChecked ?? false);
13
+ const path = `/inputs/${name}`;
19
14
  const raw = get(path);
20
- const checked = raw === undefined || raw === null ? fallback : Boolean(raw);
21
- return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [label && (_jsx(Label, { htmlFor: id, className: "text-foreground font-normal", children: label })), _jsx(Switch, { id: id, checked: checked, onCheckedChange: (v) => set(path, v), style: accentStyle, className: cn(mode === "light" &&
22
- "data-unchecked:!bg-border data-unchecked:!border-(--input-border)") })] }));
15
+ const checked = raw !== undefined ? Boolean(raw) : Boolean(props.defaultChecked);
16
+ const id = useId();
17
+ return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [label && (_jsx(Label, { htmlFor: id, className: "font-normal", style: { color: colors.text }, children: label })), _jsx(Switch, { id: id, checked: checked, onCheckedChange: (v) => set(path, v), style: {
18
+ backgroundColor: checked ? colors.accent : colors.muted,
19
+ borderColor: checked ? colors.accent : colors.inputBorder,
20
+ } })] }));
23
21
  }
@@ -1,15 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { Text, Title } from "@neynar/ui/typography";
3
+ import { Text } from "@neynar/ui/typography";
4
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
4
5
  const SIZE_MAP = {
5
- lg: { component: "title", textSize: undefined, order: 3 },
6
- md: { component: "text", textSize: "base", order: undefined },
7
- sm: { component: "text", textSize: "sm", order: undefined },
8
- };
9
- const WEIGHT_MAP = {
10
- bold: "bold",
11
- medium: "medium",
12
- normal: "normal",
6
+ md: { textSize: "base" },
7
+ sm: { textSize: "sm" },
13
8
  };
14
9
  export function SnapText({ element: { props }, }) {
15
10
  const content = String(props.content ?? "");
@@ -17,9 +12,6 @@ export function SnapText({ element: { props }, }) {
17
12
  const weight = props.weight ? String(props.weight) : undefined;
18
13
  const align = props.align ?? undefined;
19
14
  const config = SIZE_MAP[size] ?? SIZE_MAP.md;
20
- const alignClass = align === "center" ? "text-center" : align === "right" ? "text-right" : "";
21
- if (config.component === "title") {
22
- return (_jsx(Title, { order: config.order, weight: weight ?? "bold", className: `flex-1 ${alignClass}`, children: content }));
23
- }
24
- return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: "flex-1", children: content }));
15
+ const colors = useSnapColors();
16
+ return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: "flex-1", style: { color: colors.text }, children: content }));
25
17
  }
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
3
4
  import { useStateStore } from "@json-render/react";
4
5
  import { Label } from "@neynar/ui/label";
5
6
  import { cn } from "@neynar/ui/utils";
6
- import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
7
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
7
8
  export function SnapToggleGroup({ element: { props }, }) {
8
9
  const { get, set } = useStateStore();
9
- const accentStyle = useSnapAccentScopeStyle();
10
+ const colors = useSnapColors();
10
11
  const name = String(props.name ?? "toggle_group");
11
12
  const path = `/inputs/${name}`;
12
13
  const label = props.label ? String(props.label) : undefined;
@@ -43,10 +44,23 @@ export function SnapToggleGroup({ element: { props }, }) {
43
44
  }
44
45
  };
45
46
  const isVertical = orientation === "vertical";
46
- return (_jsxs("div", { className: "w-full space-y-1.5", style: accentStyle, children: [label && _jsx(Label, { children: label }), _jsx("div", { className: cn("flex gap-1 rounded-lg bg-border/20 p-1", isVertical ? "flex-col" : "flex-row"), children: options.map((opt) => {
47
+ const [hoveredIdx, setHoveredIdx] = useState(null);
48
+ return (_jsxs("div", { className: "w-full space-y-1.5", children: [label && _jsx(Label, { style: { color: colors.text }, children: label }), _jsx("div", { className: cn("flex gap-1 rounded-lg p-1", isVertical ? "flex-col" : "flex-row"), style: { backgroundColor: colors.muted }, children: options.map((opt, i) => {
47
49
  const isSelected = selected.includes(opt);
48
- return (_jsx("button", { type: "button", onClick: () => toggle(opt), className: cn("rounded-md px-3 py-2 text-sm font-medium transition-colors", isVertical ? "w-full" : "flex-1", isSelected
49
- ? "bg-primary text-primary-foreground"
50
- : "text-foreground hover:bg-border/30"), children: opt }, opt));
50
+ const isHovered = hoveredIdx === i && !isSelected;
51
+ return (_jsx("button", { type: "button", onClick: () => toggle(opt), onPointerEnter: () => setHoveredIdx(i), onPointerLeave: () => setHoveredIdx(null), className: cn("rounded-md px-3 py-2 text-sm font-medium transition-colors", isVertical ? "w-full" : "flex-1"), style: {
52
+ transition: "background-color 0.15s, color 0.15s",
53
+ ...(isSelected
54
+ ? {
55
+ backgroundColor: colors.mode === "dark" ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.08)",
56
+ color: colors.text,
57
+ }
58
+ : {
59
+ color: colors.text,
60
+ backgroundColor: isHovered
61
+ ? (colors.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)")
62
+ : (colors.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)"),
63
+ }),
64
+ }, children: opt }, opt));
51
65
  }) })] }));
52
66
  }
@@ -0,0 +1,38 @@
1
+ /** Readable foreground color (black or white) for a given hex background. */
2
+ export declare function pickForegroundForBg(hex: string): string;
3
+ export type SnapColors = {
4
+ /** Resolved accent hex */
5
+ accent: string;
6
+ /** Readable foreground for accent bg (black or white) */
7
+ accentFg: string;
8
+ /** Primary button hover color */
9
+ accentHover: string;
10
+ /** Secondary/outline button hover color */
11
+ outlineHover: string;
12
+ /** Primary text color */
13
+ text: string;
14
+ /** Muted/secondary text color */
15
+ textMuted: string;
16
+ /** Border color */
17
+ border: string;
18
+ /** Muted background (tracks, containers) */
19
+ muted: string;
20
+ /** Surface/card background */
21
+ surface: string;
22
+ /** Input border */
23
+ inputBorder: string;
24
+ /** Input background */
25
+ inputBg: string;
26
+ /** Current color mode */
27
+ mode: "light" | "dark";
28
+ /** Resolve a palette color name to hex */
29
+ paletteHex: (name: string) => string;
30
+ /** Resolve a palette color name to hex, with accent fallback */
31
+ colorHex: (name: string | undefined) => string;
32
+ };
33
+ /**
34
+ * Returns fully resolved color values for snap components.
35
+ * All colors are concrete hex values (or color-mix expressions for hover states)
36
+ * so they can be used as inline styles, independent of host app CSS.
37
+ */
38
+ export declare function useSnapColors(): SnapColors;
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { useMemo } from "react";
3
+ import { useStateStore } from "@json-render/react";
4
+ import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
5
+ import { useSnapPreviewPageAccent, useSnapAppearance } from "../accent-context.js";
6
+ import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX } from "@farcaster/snap";
7
+ /** Readable foreground color (black or white) for a given hex background. */
8
+ export function pickForegroundForBg(hex) {
9
+ const h = hex.replace(/^#/, "");
10
+ if (h.length !== 6)
11
+ return "#ffffff";
12
+ const r = Number.parseInt(h.slice(0, 2), 16);
13
+ const g = Number.parseInt(h.slice(2, 4), 16);
14
+ const b = Number.parseInt(h.slice(4, 6), 16);
15
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
16
+ return yiq >= 180 ? "#0a0a0a" : "#ffffff";
17
+ }
18
+ const NEUTRAL_LIGHT = {
19
+ text: "#111111",
20
+ textMuted: "#6B7280",
21
+ border: "#E5E7EB",
22
+ muted: "rgba(0,0,0,0.06)",
23
+ surface: "#ffffff",
24
+ inputBorder: "#E5E7EB",
25
+ inputBg: "rgba(0,0,0,0.06)",
26
+ };
27
+ const NEUTRAL_DARK = {
28
+ text: "#FAFAFA",
29
+ textMuted: "#A1A1AA",
30
+ border: "#2D2D44",
31
+ muted: "rgba(255,255,255,0.03)",
32
+ surface: "#23262f",
33
+ inputBorder: "#3F3F46",
34
+ inputBg: "rgba(255,255,255,0.03)",
35
+ };
36
+ function buildSnapColors(accentName, mode) {
37
+ const accent = resolveSnapPaletteHex(accentName, mode);
38
+ const accentFg = pickForegroundForBg(accent);
39
+ const neutrals = mode === "dark" ? NEUTRAL_DARK : NEUTRAL_LIGHT;
40
+ const paletteMap = mode === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
41
+ const accentHover = mode === "light"
42
+ ? `color-mix(in srgb, ${accent} 82%, #000000)`
43
+ : `color-mix(in srgb, ${accent} 78%, #ffffff)`;
44
+ const outlineHover = `color-mix(in srgb, ${accent} 14%, ${neutrals.surface})`;
45
+ const paletteHex = (name) => resolveSnapPaletteHex(name, mode);
46
+ const colorHex = (name) => {
47
+ if (!name || name === "accent")
48
+ return accent;
49
+ if (Object.hasOwn(paletteMap, name))
50
+ return paletteMap[name];
51
+ return accent;
52
+ };
53
+ return {
54
+ accent,
55
+ accentFg,
56
+ accentHover,
57
+ outlineHover,
58
+ ...neutrals,
59
+ mode,
60
+ paletteHex,
61
+ colorHex,
62
+ };
63
+ }
64
+ /**
65
+ * Returns fully resolved color values for snap components.
66
+ * All colors are concrete hex values (or color-mix expressions for hover states)
67
+ * so they can be used as inline styles, independent of host app CSS.
68
+ */
69
+ export function useSnapColors() {
70
+ const { get } = useStateStore();
71
+ const mode = useSnapAppearance();
72
+ const pageAccent = useSnapPreviewPageAccent();
73
+ const fromState = get("/theme/accent");
74
+ const accentRaw = (typeof pageAccent === "string" && pageAccent.length > 0
75
+ ? pageAccent
76
+ : fromState) ?? undefined;
77
+ const accentName = typeof accentRaw === "string" && accentRaw.length > 0
78
+ ? accentRaw
79
+ : "purple";
80
+ return useMemo(() => buildSnapColors(accentName, mode), [accentName, mode]);
81
+ }
@@ -1,4 +1,6 @@
1
1
  import type { Spec } from "@json-render/core";
2
+ import type { ReactNode } from "react";
3
+ import type { ValidationResult } from "../validator.js";
2
4
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
5
  [key: string]: JsonValue;
4
6
  };
@@ -13,6 +15,7 @@ export type SnapPage = {
13
15
  export type SnapActionHandlers = {
14
16
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
15
17
  open_url: (target: string) => void;
18
+ open_snap: (target: string) => void;
16
19
  open_mini_app: (target: string) => void;
17
20
  view_cast: (params: {
18
21
  hash: string;
@@ -39,9 +42,18 @@ export type SnapActionHandlers = {
39
42
  buyToken?: string;
40
43
  }) => void;
41
44
  };
42
- export declare function SnapView({ snap, handlers, loading, appearance, }: {
45
+ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
43
46
  snap: SnapPage;
44
47
  handlers: SnapActionHandlers;
45
48
  loading?: boolean;
46
49
  appearance?: "light" | "dark";
50
+ maxWidth?: number;
51
+ /** When true, extends to 700px and shows a warning overlay below 500px. When false, clips at 500px. Only applies to v2 snaps. */
52
+ showOverflowWarning?: boolean;
53
+ onValidationError?: (result: ValidationResult) => void;
54
+ validationErrorFallback?: ReactNode;
55
+ /** Server-side action error message to display inline. */
56
+ actionError?: string | null;
57
+ /** When true, renders without card frame (no border, background, or padding). */
58
+ plain?: boolean;
47
59
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,191 +1,12 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { snapJsonRenderCatalog } from "../ui/index.js";
4
- import { SnapCatalogView } from "./catalog-renderer.js";
5
- import { SnapPreviewAccentProvider } from "./accent-context.js";
6
- import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex.js";
7
- import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css.js";
8
- import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
9
- // ─── Internal helpers ──────────────────────────────────
10
- function applyStatePaths(model, changes) {
11
- const entries = Array.isArray(changes)
12
- ? changes.map((c) => [c.path, c.value])
13
- : Object.entries(changes);
14
- for (const [path, value] of entries) {
15
- const trimmed = path.startsWith("/") ? path : `/${path}`;
16
- const parts = trimmed.split("/").filter(Boolean);
17
- if (parts.length < 2)
18
- continue;
19
- const [top, ...rest] = parts;
20
- if (top === "inputs") {
21
- if (typeof model.inputs !== "object" || model.inputs === null) {
22
- model.inputs = {};
23
- }
24
- const inputs = model.inputs;
25
- if (rest.length === 1) {
26
- inputs[rest[0]] = value;
27
- }
28
- continue;
29
- }
30
- if (top === "theme") {
31
- if (typeof model.theme !== "object" || model.theme === null) {
32
- model.theme = {};
33
- }
34
- const theme = model.theme;
35
- if (rest.length === 1) {
36
- theme[rest[0]] = value;
37
- }
38
- }
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { SPEC_VERSION_2 } from "../constants.js";
4
+ import { SnapCardV1 } from "./v1/snap-view.js";
5
+ import { SnapCardV2 } from "./v2/snap-view.js";
6
+ // ─── SnapCard ────────────────────────────────────────
7
+ export function SnapCard({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
8
+ if (snap.version === SPEC_VERSION_2) {
9
+ return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain }));
39
10
  }
40
- }
41
- const CONFETTI_COLORS = [
42
- "#8B5CF6",
43
- "#EC4899",
44
- "#3B82F6",
45
- "#10B981",
46
- "#F59E0B",
47
- "#EF4444",
48
- "#06B6D4",
49
- ];
50
- function ConfettiOverlay() {
51
- const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) => ({
52
- id: i,
53
- left: Math.random() * 100,
54
- delay: Math.random() * 1.2,
55
- duration: 2.5 + Math.random() * 2,
56
- color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
57
- size: 6 + Math.random() * 8,
58
- rotation: Math.random() * 360,
59
- })), []);
60
- return (_jsxs("div", { style: {
61
- position: "absolute",
62
- inset: 0,
63
- overflow: "hidden",
64
- pointerEvents: "none",
65
- zIndex: 20,
66
- }, children: [pieces.map(({ id, left, delay, duration, color, size, rotation }) => (_jsx("div", { style: {
67
- position: "absolute",
68
- left: `${left}%`,
69
- top: -20,
70
- width: size,
71
- height: size * 0.6,
72
- backgroundColor: color,
73
- borderRadius: 2,
74
- transform: `rotate(${rotation}deg)`,
75
- animation: `confettiFall ${duration}s ease-in ${delay}s forwards`,
76
- } }, id))), _jsx("style", { children: `@keyframes confettiFall{0%{top:-20px;opacity:1;transform:rotate(0deg) translateX(0)}50%{opacity:1}100%{top:110%;opacity:0;transform:rotate(720deg) translateX(${Math.random() > 0.5 ? "" : "-"}40px)}}` })] }));
77
- }
78
- const PALETTE = [
79
- "gray",
80
- "blue",
81
- "red",
82
- "amber",
83
- "green",
84
- "teal",
85
- "purple",
86
- "pink",
87
- ];
88
- // ─── SnapView ──────────────────────────────────────────
89
- export function SnapView({ snap, handlers, loading = false, appearance = "dark", }) {
90
- const spec = snap.ui;
91
- const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
92
- const stateRef = useRef(initialState);
93
- useEffect(() => {
94
- stateRef.current = {
95
- inputs: {
96
- ...(initialState.inputs ?? {}),
97
- },
98
- theme: {
99
- ...(initialState.theme ?? {}),
100
- },
101
- };
102
- }, [initialState]);
103
- useEffect(() => {
104
- const result = snapJsonRenderCatalog.validate(spec);
105
- if (!result.success) {
106
- // eslint-disable-next-line no-console
107
- console.warn("[SnapView] catalog validation issues:", result.error);
108
- }
109
- }, [spec]);
110
- const [pageKey, setPageKey] = useState(0);
111
- useEffect(() => {
112
- setPageKey((k) => k + 1);
113
- }, [spec]);
114
- const showConfetti = snap.effects?.includes("confetti");
115
- const previewSurfaceStyle = useMemo(() => {
116
- const vars = {};
117
- for (const c of PALETTE)
118
- vars[`--snap-color-${c}`] = resolveSnapPaletteHex(c, appearance);
119
- return {
120
- ...snapPreviewPrimaryCssProperties(snap.theme?.accent ?? "purple", appearance),
121
- ...vars,
122
- };
123
- }, [snap.theme?.accent, appearance]);
124
- const handleAction = useCallback((name, params) => {
125
- const inputs = (stateRef.current.inputs ?? {});
126
- const p = (params ?? {});
127
- switch (name) {
128
- case "submit":
129
- handlers.submit(String(p.target ?? ""), inputs);
130
- break;
131
- case "open_url":
132
- handlers.open_url(String(p.target ?? ""));
133
- break;
134
- case "open_mini_app":
135
- handlers.open_mini_app(String(p.target ?? ""));
136
- break;
137
- case "view_cast":
138
- handlers.view_cast({ hash: String(p.hash ?? "") });
139
- break;
140
- case "view_profile":
141
- handlers.view_profile({ fid: Number(p.fid ?? 0) });
142
- break;
143
- case "compose_cast":
144
- handlers.compose_cast({
145
- text: p.text ? String(p.text) : undefined,
146
- channelKey: p.channelKey ? String(p.channelKey) : undefined,
147
- embeds: Array.isArray(p.embeds)
148
- ? p.embeds
149
- : undefined,
150
- });
151
- break;
152
- case "view_token":
153
- handlers.view_token({ token: String(p.token ?? "") });
154
- break;
155
- case "send_token":
156
- handlers.send_token({
157
- token: String(p.token ?? ""),
158
- amount: p.amount ? String(p.amount) : undefined,
159
- recipientFid: p.recipientFid
160
- ? Number(p.recipientFid)
161
- : undefined,
162
- recipientAddress: p.recipientAddress
163
- ? String(p.recipientAddress)
164
- : undefined,
165
- });
166
- break;
167
- case "swap_token":
168
- handlers.swap_token({
169
- sellToken: p.sellToken ? String(p.sellToken) : undefined,
170
- buyToken: p.buyToken ? String(p.buyToken) : undefined,
171
- });
172
- break;
173
- default:
174
- break;
175
- }
176
- }, [handlers]);
177
- return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}), loading && (_jsx("div", { style: {
178
- position: "absolute",
179
- inset: 0,
180
- display: "flex",
181
- alignItems: "center",
182
- justifyContent: "center",
183
- zIndex: 10,
184
- fontSize: 14,
185
- color: "var(--text-muted)",
186
- background: "var(--bg-primary, rgba(0,0,0,0.6))",
187
- backdropFilter: "blur(4px)",
188
- }, children: "Loading..." })), _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
189
- applyStatePaths(stateRef.current, changes);
190
- }, onAction: handleAction }, pageKey) }) })] }));
11
+ return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, actionError: actionError, plain: plain }));
191
12
  }
@@ -0,0 +1,11 @@
1
+ import type { SnapActionHandlers, SnapPage } from "./index.js";
2
+ export declare function applyStatePaths(model: Record<string, unknown>, changes: {
3
+ path: string;
4
+ value: unknown;
5
+ }[] | Record<string, unknown>): void;
6
+ export declare function SnapViewCore({ snap, handlers, loading, appearance, }: {
7
+ snap: SnapPage;
8
+ handlers: SnapActionHandlers;
9
+ loading?: boolean;
10
+ appearance?: "light" | "dark";
11
+ }): import("react/jsx-runtime").JSX.Element;