@farcaster/snap 1.5.1 → 2.0.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 (156) hide show
  1. package/dist/constants.d.ts +0 -107
  2. package/dist/constants.js +0 -148
  3. package/dist/dataStore.d.ts +12 -0
  4. package/dist/dataStore.js +35 -0
  5. package/dist/index.d.ts +6 -3
  6. package/dist/index.js +5 -3
  7. package/dist/middleware.d.ts +3 -0
  8. package/dist/middleware.js +3 -0
  9. package/dist/react/accent-context.d.ts +6 -0
  10. package/dist/react/accent-context.js +10 -0
  11. package/dist/react/catalog-renderer.d.ts +5 -0
  12. package/dist/react/catalog-renderer.js +37 -0
  13. package/dist/react/components/action-button.d.ts +6 -0
  14. package/dist/react/components/action-button.js +22 -0
  15. package/dist/react/components/badge.d.ts +5 -0
  16. package/dist/react/components/badge.js +18 -0
  17. package/dist/react/components/icon.d.ts +7 -0
  18. package/dist/react/components/icon.js +60 -0
  19. package/dist/react/components/image.d.ts +5 -0
  20. package/dist/react/components/image.js +15 -0
  21. package/dist/react/components/input.d.ts +5 -0
  22. package/dist/react/components/input.js +18 -0
  23. package/dist/react/components/item-group.d.ts +7 -0
  24. package/dist/react/components/item-group.js +17 -0
  25. package/dist/react/components/item.d.ts +7 -0
  26. package/dist/react/components/item.js +9 -0
  27. package/dist/react/components/progress.d.ts +5 -0
  28. package/dist/react/components/progress.js +11 -0
  29. package/dist/react/components/separator.d.ts +5 -0
  30. package/dist/react/components/separator.js +7 -0
  31. package/dist/react/components/slider.d.ts +5 -0
  32. package/dist/react/components/slider.js +21 -0
  33. package/dist/react/components/stack.d.ts +7 -0
  34. package/dist/react/components/stack.js +32 -0
  35. package/dist/react/components/switch.d.ts +5 -0
  36. package/dist/react/components/switch.js +23 -0
  37. package/dist/react/components/text.d.ts +5 -0
  38. package/dist/react/components/text.js +25 -0
  39. package/dist/react/components/toggle-group.d.ts +5 -0
  40. package/dist/react/components/toggle-group.js +52 -0
  41. package/dist/react/hooks/use-snap-accent.d.ts +13 -0
  42. package/dist/react/hooks/use-snap-accent.js +32 -0
  43. package/dist/react/index.d.ts +47 -0
  44. package/dist/react/index.js +191 -0
  45. package/dist/react/lib/preview-primary-css.d.ts +6 -0
  46. package/dist/react/lib/preview-primary-css.js +43 -0
  47. package/dist/react/lib/resolve-palette-hex.d.ts +2 -0
  48. package/dist/react/lib/resolve-palette-hex.js +10 -0
  49. package/dist/schemas.d.ts +14 -1629
  50. package/dist/schemas.js +14 -526
  51. package/dist/ui/badge.d.ts +52 -0
  52. package/dist/ui/badge.js +9 -0
  53. package/dist/ui/button.d.ts +42 -28
  54. package/dist/ui/button.js +7 -9
  55. package/dist/ui/catalog.d.ts +280 -155
  56. package/dist/ui/catalog.js +102 -83
  57. package/dist/ui/icon.d.ts +56 -0
  58. package/dist/ui/icon.js +51 -0
  59. package/dist/ui/image.d.ts +1 -0
  60. package/dist/ui/image.js +2 -2
  61. package/dist/ui/index.d.ts +20 -22
  62. package/dist/ui/index.js +10 -11
  63. package/dist/ui/input.d.ts +17 -0
  64. package/dist/ui/input.js +13 -0
  65. package/dist/ui/item-group.d.ts +12 -0
  66. package/dist/ui/item-group.js +7 -0
  67. package/dist/ui/item.d.ts +14 -0
  68. package/dist/ui/item.js +9 -0
  69. package/dist/ui/progress.d.ts +1 -11
  70. package/dist/ui/progress.js +21 -4
  71. package/dist/ui/schema.js +3 -3
  72. package/dist/ui/separator.d.ts +9 -0
  73. package/dist/ui/separator.js +5 -0
  74. package/dist/ui/slider.d.ts +4 -3
  75. package/dist/ui/slider.js +34 -5
  76. package/dist/ui/stack.d.ts +22 -1
  77. package/dist/ui/stack.js +8 -1
  78. package/dist/ui/switch.d.ts +8 -0
  79. package/dist/ui/switch.js +7 -0
  80. package/dist/ui/text.d.ts +15 -7
  81. package/dist/ui/text.js +8 -4
  82. package/dist/ui/toggle-group.d.ts +23 -0
  83. package/dist/ui/toggle-group.js +19 -0
  84. package/dist/validator.d.ts +5 -1
  85. package/dist/validator.js +6 -136
  86. package/package.json +72 -52
  87. package/src/constants.ts +0 -179
  88. package/src/dataStore.ts +62 -0
  89. package/src/index.ts +11 -20
  90. package/src/middleware.ts +7 -0
  91. package/src/react/accent-context.tsx +29 -0
  92. package/src/react/catalog-renderer.tsx +39 -0
  93. package/src/react/components/action-button.tsx +48 -0
  94. package/src/react/components/badge.tsx +37 -0
  95. package/src/react/components/icon.tsx +115 -0
  96. package/src/react/components/image.tsx +33 -0
  97. package/src/react/components/input.tsx +36 -0
  98. package/src/react/components/item-group.tsx +43 -0
  99. package/src/react/components/item.tsx +33 -0
  100. package/src/react/components/progress.tsx +29 -0
  101. package/src/react/components/separator.tsx +14 -0
  102. package/src/react/components/slider.tsx +43 -0
  103. package/src/react/components/stack.tsx +55 -0
  104. package/src/react/components/switch.tsx +46 -0
  105. package/src/react/components/text.tsx +43 -0
  106. package/src/react/components/toggle-group.tsx +85 -0
  107. package/src/react/hooks/use-snap-accent.ts +45 -0
  108. package/src/react/index.tsx +321 -0
  109. package/src/react/lib/preview-primary-css.ts +57 -0
  110. package/src/react/lib/resolve-palette-hex.ts +20 -0
  111. package/src/schemas.ts +18 -644
  112. package/src/ui/badge.ts +13 -0
  113. package/src/ui/button.ts +9 -12
  114. package/src/ui/catalog.ts +106 -86
  115. package/src/ui/icon.ts +56 -0
  116. package/src/ui/image.ts +3 -2
  117. package/src/ui/index.ts +26 -29
  118. package/src/ui/input.ts +17 -0
  119. package/src/ui/item-group.ts +11 -0
  120. package/src/ui/item.ts +13 -0
  121. package/src/ui/progress.ts +25 -7
  122. package/src/ui/schema.ts +3 -3
  123. package/src/ui/separator.ts +9 -0
  124. package/src/ui/slider.ts +40 -10
  125. package/src/ui/stack.ts +9 -1
  126. package/src/ui/switch.ts +11 -0
  127. package/src/ui/text.ts +9 -4
  128. package/src/ui/toggle-group.ts +23 -0
  129. package/src/validator.ts +6 -176
  130. package/dist/ui/bar-chart.d.ts +0 -30
  131. package/dist/ui/bar-chart.js +0 -15
  132. package/dist/ui/button-group.d.ts +0 -19
  133. package/dist/ui/button-group.js +0 -18
  134. package/dist/ui/divider.d.ts +0 -3
  135. package/dist/ui/divider.js +0 -2
  136. package/dist/ui/grid.d.ts +0 -22
  137. package/dist/ui/grid.js +0 -16
  138. package/dist/ui/group.d.ts +0 -7
  139. package/dist/ui/group.js +0 -5
  140. package/dist/ui/list.d.ts +0 -13
  141. package/dist/ui/list.js +0 -13
  142. package/dist/ui/spacer.d.ts +0 -9
  143. package/dist/ui/spacer.js +0 -5
  144. package/dist/ui/text-input.d.ts +0 -7
  145. package/dist/ui/text-input.js +0 -12
  146. package/dist/ui/toggle.d.ts +0 -7
  147. package/dist/ui/toggle.js +0 -6
  148. package/src/ui/bar-chart.ts +0 -20
  149. package/src/ui/button-group.ts +0 -26
  150. package/src/ui/divider.ts +0 -5
  151. package/src/ui/grid.ts +0 -25
  152. package/src/ui/group.ts +0 -8
  153. package/src/ui/list.ts +0 -17
  154. package/src/ui/spacer.ts +0 -8
  155. package/src/ui/text-input.ts +0 -15
  156. package/src/ui/toggle.ts +0 -9
@@ -0,0 +1,5 @@
1
+ export declare function SnapSeparator({ element: { props }, }: {
2
+ element: {
3
+ props: Record<string, unknown>;
4
+ };
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Separator } from "@neynar/ui/separator";
4
+ export function SnapSeparator({ element: { props }, }) {
5
+ const orientation = props.orientation ?? "horizontal";
6
+ return _jsx(Separator, { orientation: orientation });
7
+ }
@@ -0,0 +1,5 @@
1
+ export declare function SnapSlider({ element: { props }, }: {
2
+ element: {
3
+ props: Record<string, unknown>;
4
+ };
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useStateStore } from "@json-render/react";
4
+ import { Label } from "@neynar/ui/label";
5
+ import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
6
+ // TODO: switch back to @neynar/ui/slider once Base UI fixes the inline
7
+ // <script> tag that triggers a React console warning on client render.
8
+ export function SnapSlider({ element: { props }, }) {
9
+ const { get, set } = useStateStore();
10
+ const accentStyle = useSnapAccentScopeStyle();
11
+ const name = String(props.name ?? "slider");
12
+ const path = `/inputs/${name}`;
13
+ const label = props.label ? String(props.label) : undefined;
14
+ const min = Number(props.min ?? 0);
15
+ const max = Number(props.max ?? 100);
16
+ const step = props.step != null ? Number(props.step) : 1;
17
+ const fallback = props.defaultValue != null ? Number(props.defaultValue) : (min + max) / 2;
18
+ const raw = get(path);
19
+ const value = raw === undefined || raw === null ? fallback : Number(raw);
20
+ return (_jsxs("div", { className: "flex w-full flex-col gap-1.5", style: accentStyle, children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "range", min: min, max: max, step: step, value: value, onChange: (e) => set(path, Number(e.target.value)), className: "w-full h-2.5 rounded-full appearance-none bg-muted cursor-pointer", style: { accentColor: "var(--primary)" } })] }));
21
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ export declare function SnapStack({ element: { props }, children, }: {
3
+ element: {
4
+ props: Record<string, unknown>;
5
+ };
6
+ children?: ReactNode;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { cn } from "@neynar/ui/utils";
4
+ const VGAP = {
5
+ none: "gap-0",
6
+ sm: "gap-2",
7
+ md: "gap-4",
8
+ lg: "gap-6",
9
+ };
10
+ const HGAP = {
11
+ none: "gap-0",
12
+ sm: "gap-1",
13
+ md: "gap-2",
14
+ lg: "gap-3",
15
+ };
16
+ const JUSTIFY = {
17
+ start: "justify-start",
18
+ center: "justify-center",
19
+ end: "justify-end",
20
+ between: "justify-between",
21
+ around: "justify-around",
22
+ };
23
+ export function SnapStack({ element: { props }, children, }) {
24
+ const direction = String(props.direction ?? "vertical");
25
+ const gapKey = String(props.gap ?? "md");
26
+ const isHorizontal = direction === "horizontal";
27
+ const gap = isHorizontal
28
+ ? (HGAP[gapKey] ?? "gap-2")
29
+ : (VGAP[gapKey] ?? "gap-4");
30
+ const justify = props.justify ? JUSTIFY[String(props.justify)] : undefined;
31
+ return (_jsx("div", { className: cn("flex w-full", isHorizontal ? "flex-row items-center flex-wrap" : "flex-col", gap, justify), children: children }));
32
+ }
@@ -0,0 +1,5 @@
1
+ export declare function SnapSwitch({ element: { props }, }: {
2
+ element: {
3
+ props: Record<string, unknown>;
4
+ };
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { useStateStore } from "@json-render/react";
5
+ import { Label } from "@neynar/ui/label";
6
+ 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";
10
+ export function SnapSwitch({ element: { props }, }) {
11
+ const id = useId();
12
+ const { get, set } = useStateStore();
13
+ const { mode } = useColorMode();
14
+ const accentStyle = useSnapAccentScopeStyle();
15
+ const name = String(props.name ?? "switch");
16
+ const path = `/inputs/${name}`;
17
+ const label = props.label ? String(props.label) : undefined;
18
+ const fallback = Boolean(props.defaultChecked ?? false);
19
+ 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)") })] }));
23
+ }
@@ -0,0 +1,5 @@
1
+ export declare function SnapText({ element: { props }, }: {
2
+ element: {
3
+ props: Record<string, unknown>;
4
+ };
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Text, Title } from "@neynar/ui/typography";
4
+ 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",
13
+ };
14
+ export function SnapText({ element: { props }, }) {
15
+ const content = String(props.content ?? "");
16
+ const size = String(props.size ?? "md");
17
+ const weight = props.weight ? String(props.weight) : undefined;
18
+ const align = props.align ?? undefined;
19
+ 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 }));
25
+ }
@@ -0,0 +1,5 @@
1
+ export declare function SnapToggleGroup({ element: { props }, }: {
2
+ element: {
3
+ props: Record<string, unknown>;
4
+ };
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useStateStore } from "@json-render/react";
4
+ import { Label } from "@neynar/ui/label";
5
+ import { cn } from "@neynar/ui/utils";
6
+ import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
7
+ export function SnapToggleGroup({ element: { props }, }) {
8
+ const { get, set } = useStateStore();
9
+ const accentStyle = useSnapAccentScopeStyle();
10
+ const name = String(props.name ?? "toggle_group");
11
+ const path = `/inputs/${name}`;
12
+ const label = props.label ? String(props.label) : undefined;
13
+ const isMultiple = Boolean(props.multiple);
14
+ const orientation = String(props.orientation ?? "horizontal");
15
+ const options = Array.isArray(props.options)
16
+ ? props.options
17
+ : [];
18
+ const raw = get(path);
19
+ const defaultValue = props.defaultValue;
20
+ const selected = (() => {
21
+ if (raw !== undefined && raw !== null) {
22
+ return isMultiple
23
+ ? Array.isArray(raw) ? raw : []
24
+ : typeof raw === "string" ? [raw] : [];
25
+ }
26
+ if (defaultValue !== undefined) {
27
+ return Array.isArray(defaultValue) ? defaultValue : [String(defaultValue)];
28
+ }
29
+ return [];
30
+ })();
31
+ const toggle = (opt) => {
32
+ if (isMultiple) {
33
+ const current = Array.isArray(raw) ? raw : [];
34
+ if (current.includes(opt)) {
35
+ set(path, current.filter((v) => v !== opt));
36
+ }
37
+ else {
38
+ set(path, [...current, opt]);
39
+ }
40
+ }
41
+ else {
42
+ set(path, opt);
43
+ }
44
+ };
45
+ 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 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));
51
+ }) })] }));
52
+ }
@@ -0,0 +1,13 @@
1
+ import { type CSSProperties } from "react";
2
+ import type { PaletteColor } from "@farcaster/snap";
3
+ /**
4
+ * CSS variables so Neynar controls (`bg-primary`, `data-checked:bg-primary`, etc.)
5
+ * use the snap `theme.accent` inside json-render catalog components.
6
+ */
7
+ export declare function useSnapAccentScopeStyle(): CSSProperties;
8
+ /** Active snap palette table for the current docs shell theme. */
9
+ export declare function useSnapPalette(): {
10
+ hex: (name: string) => string;
11
+ map: Record<PaletteColor, string>;
12
+ theme: "light" | "dark";
13
+ };
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { useMemo } from "react";
3
+ import { useStateStore } from "@json-render/react";
4
+ import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX } from "@farcaster/snap";
5
+ import { useColorMode } from "@neynar/ui/color-mode";
6
+ import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
7
+ import { snapPreviewPrimaryCssProperties } from "../lib/preview-primary-css.js";
8
+ import { useSnapPreviewPageAccent } from "../accent-context.js";
9
+ /**
10
+ * CSS variables so Neynar controls (`bg-primary`, `data-checked:bg-primary`, etc.)
11
+ * use the snap `theme.accent` inside json-render catalog components.
12
+ */
13
+ export function useSnapAccentScopeStyle() {
14
+ const { get } = useStateStore();
15
+ const { mode } = useColorMode();
16
+ const pageAccent = useSnapPreviewPageAccent();
17
+ const fromState = get("/theme/accent");
18
+ const accentRaw = (typeof pageAccent === "string" && pageAccent.length > 0
19
+ ? pageAccent
20
+ : fromState) ?? undefined;
21
+ const accentName = typeof accentRaw === "string" && accentRaw.length > 0
22
+ ? accentRaw
23
+ : "purple";
24
+ return useMemo(() => snapPreviewPrimaryCssProperties(accentName, mode), [accentName, mode]);
25
+ }
26
+ /** Active snap palette table for the current docs shell theme. */
27
+ export function useSnapPalette() {
28
+ const { mode } = useColorMode();
29
+ const map = mode === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
30
+ const hex = (name) => resolveSnapPaletteHex(name, mode);
31
+ return { hex, map, theme: mode };
32
+ }
@@ -0,0 +1,47 @@
1
+ import type { Spec } from "@json-render/core";
2
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type SnapPage = {
6
+ version: string;
7
+ theme?: {
8
+ accent?: string;
9
+ };
10
+ effects?: string[];
11
+ ui: Spec;
12
+ };
13
+ export type SnapActionHandlers = {
14
+ submit: (target: string, inputs: Record<string, JsonValue>) => void;
15
+ open_url: (target: string) => void;
16
+ open_mini_app: (target: string) => void;
17
+ view_cast: (params: {
18
+ hash: string;
19
+ }) => void;
20
+ view_profile: (params: {
21
+ fid: number;
22
+ }) => void;
23
+ compose_cast: (params: {
24
+ text?: string;
25
+ channelKey?: string;
26
+ embeds?: string[];
27
+ }) => void;
28
+ view_token: (params: {
29
+ token: string;
30
+ }) => void;
31
+ send_token: (params: {
32
+ token: string;
33
+ amount?: string;
34
+ recipientFid?: number;
35
+ recipientAddress?: string;
36
+ }) => void;
37
+ swap_token: (params: {
38
+ sellToken?: string;
39
+ buyToken?: string;
40
+ }) => void;
41
+ };
42
+ export declare function SnapView({ snap, handlers, loading, appearance, }: {
43
+ snap: SnapPage;
44
+ handlers: SnapActionHandlers;
45
+ loading?: boolean;
46
+ appearance?: "light" | "dark";
47
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,191 @@
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
+ }
39
+ }
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) }) })] }));
191
+ }
@@ -0,0 +1,6 @@
1
+ import type { CSSProperties } from "react";
2
+ /**
3
+ * Overrides Neynar / Tailwind theme tokens so `bg-primary`, `border-primary`, etc.
4
+ * use the snap spec accent inside the preview subtree.
5
+ */
6
+ export declare function snapPreviewPrimaryCssProperties(accentName: string, appearance: "light" | "dark"): CSSProperties;
@@ -0,0 +1,43 @@
1
+ import { resolveSnapPaletteHex } from "./resolve-palette-hex.js";
2
+ /** Readable on-primary text for hex backgrounds (e.g. amber vs purple). */
3
+ function pickForegroundForBg(hex) {
4
+ const h = hex.replace(/^#/, "");
5
+ if (h.length !== 6)
6
+ return "#ffffff";
7
+ const r = Number.parseInt(h.slice(0, 2), 16);
8
+ const g = Number.parseInt(h.slice(2, 4), 16);
9
+ const b = Number.parseInt(h.slice(4, 6), 16);
10
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
11
+ return yiq >= 180 ? "#0a0a0a" : "#ffffff";
12
+ }
13
+ /** Match `globals.css` `--snap-card-bg` so hover tints sit on the preview card. */
14
+ const SNAP_CARD_BG = {
15
+ light: "#ffffff",
16
+ dark: "#23262f",
17
+ };
18
+ function snapActionPrimaryHover(hex, appearance) {
19
+ return appearance === "light"
20
+ ? `color-mix(in srgb, ${hex} 82%, #000000)`
21
+ : `color-mix(in srgb, ${hex} 78%, #ffffff)`;
22
+ }
23
+ function snapActionOutlineHover(hex, appearance) {
24
+ const card = SNAP_CARD_BG[appearance];
25
+ return `color-mix(in srgb, ${hex} 14%, ${card})`;
26
+ }
27
+ /**
28
+ * Overrides Neynar / Tailwind theme tokens so `bg-primary`, `border-primary`, etc.
29
+ * use the snap spec accent inside the preview subtree.
30
+ */
31
+ export function snapPreviewPrimaryCssProperties(accentName, appearance) {
32
+ const hex = resolveSnapPaletteHex(accentName, appearance);
33
+ const fg = pickForegroundForBg(hex);
34
+ return {
35
+ "--primary": hex,
36
+ "--primary-foreground": fg,
37
+ "--ring": hex,
38
+ "--color-primary": hex,
39
+ "--color-primary-foreground": fg,
40
+ "--snap-action-primary-hover": snapActionPrimaryHover(hex, appearance),
41
+ "--snap-action-outline-hover": snapActionOutlineHover(hex, appearance),
42
+ };
43
+ }
@@ -0,0 +1,2 @@
1
+ /** Resolve a snap palette color name to hex for the current shell appearance. */
2
+ export declare function resolveSnapPaletteHex(name: string, appearance: "light" | "dark"): string;
@@ -0,0 +1,10 @@
1
+ import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX, } from "@farcaster/snap";
2
+ /** Resolve a snap palette color name to hex for the current shell appearance. */
3
+ export function resolveSnapPaletteHex(name, appearance) {
4
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
5
+ if (Object.hasOwn(map, name) &&
6
+ typeof map[name] === "string") {
7
+ return map[name];
8
+ }
9
+ return map.purple;
10
+ }