@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.
- package/dist/constants.d.ts +0 -107
- package/dist/constants.js +0 -148
- package/dist/dataStore.d.ts +12 -0
- package/dist/dataStore.js +35 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -3
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.js +3 -0
- package/dist/react/accent-context.d.ts +6 -0
- package/dist/react/accent-context.js +10 -0
- package/dist/react/catalog-renderer.d.ts +5 -0
- package/dist/react/catalog-renderer.js +37 -0
- package/dist/react/components/action-button.d.ts +6 -0
- package/dist/react/components/action-button.js +22 -0
- package/dist/react/components/badge.d.ts +5 -0
- package/dist/react/components/badge.js +18 -0
- package/dist/react/components/icon.d.ts +7 -0
- package/dist/react/components/icon.js +60 -0
- package/dist/react/components/image.d.ts +5 -0
- package/dist/react/components/image.js +15 -0
- package/dist/react/components/input.d.ts +5 -0
- package/dist/react/components/input.js +18 -0
- package/dist/react/components/item-group.d.ts +7 -0
- package/dist/react/components/item-group.js +17 -0
- package/dist/react/components/item.d.ts +7 -0
- package/dist/react/components/item.js +9 -0
- package/dist/react/components/progress.d.ts +5 -0
- package/dist/react/components/progress.js +11 -0
- package/dist/react/components/separator.d.ts +5 -0
- package/dist/react/components/separator.js +7 -0
- package/dist/react/components/slider.d.ts +5 -0
- package/dist/react/components/slider.js +21 -0
- package/dist/react/components/stack.d.ts +7 -0
- package/dist/react/components/stack.js +32 -0
- package/dist/react/components/switch.d.ts +5 -0
- package/dist/react/components/switch.js +23 -0
- package/dist/react/components/text.d.ts +5 -0
- package/dist/react/components/text.js +25 -0
- package/dist/react/components/toggle-group.d.ts +5 -0
- package/dist/react/components/toggle-group.js +52 -0
- package/dist/react/hooks/use-snap-accent.d.ts +13 -0
- package/dist/react/hooks/use-snap-accent.js +32 -0
- package/dist/react/index.d.ts +47 -0
- package/dist/react/index.js +191 -0
- package/dist/react/lib/preview-primary-css.d.ts +6 -0
- package/dist/react/lib/preview-primary-css.js +43 -0
- package/dist/react/lib/resolve-palette-hex.d.ts +2 -0
- package/dist/react/lib/resolve-palette-hex.js +10 -0
- package/dist/schemas.d.ts +14 -1629
- package/dist/schemas.js +14 -526
- package/dist/ui/badge.d.ts +52 -0
- package/dist/ui/badge.js +9 -0
- package/dist/ui/button.d.ts +42 -28
- package/dist/ui/button.js +7 -9
- package/dist/ui/catalog.d.ts +280 -155
- package/dist/ui/catalog.js +102 -83
- package/dist/ui/icon.d.ts +56 -0
- package/dist/ui/icon.js +51 -0
- package/dist/ui/image.d.ts +1 -0
- package/dist/ui/image.js +2 -2
- package/dist/ui/index.d.ts +20 -22
- package/dist/ui/index.js +10 -11
- package/dist/ui/input.d.ts +17 -0
- package/dist/ui/input.js +13 -0
- package/dist/ui/item-group.d.ts +12 -0
- package/dist/ui/item-group.js +7 -0
- package/dist/ui/item.d.ts +14 -0
- package/dist/ui/item.js +9 -0
- package/dist/ui/progress.d.ts +1 -11
- package/dist/ui/progress.js +21 -4
- package/dist/ui/schema.js +3 -3
- package/dist/ui/separator.d.ts +9 -0
- package/dist/ui/separator.js +5 -0
- package/dist/ui/slider.d.ts +4 -3
- package/dist/ui/slider.js +34 -5
- package/dist/ui/stack.d.ts +22 -1
- package/dist/ui/stack.js +8 -1
- package/dist/ui/switch.d.ts +8 -0
- package/dist/ui/switch.js +7 -0
- package/dist/ui/text.d.ts +15 -7
- package/dist/ui/text.js +8 -4
- package/dist/ui/toggle-group.d.ts +23 -0
- package/dist/ui/toggle-group.js +19 -0
- package/dist/validator.d.ts +5 -1
- package/dist/validator.js +6 -136
- package/package.json +72 -52
- package/src/constants.ts +0 -179
- package/src/dataStore.ts +62 -0
- package/src/index.ts +11 -20
- package/src/middleware.ts +7 -0
- package/src/react/accent-context.tsx +29 -0
- package/src/react/catalog-renderer.tsx +39 -0
- package/src/react/components/action-button.tsx +48 -0
- package/src/react/components/badge.tsx +37 -0
- package/src/react/components/icon.tsx +115 -0
- package/src/react/components/image.tsx +33 -0
- package/src/react/components/input.tsx +36 -0
- package/src/react/components/item-group.tsx +43 -0
- package/src/react/components/item.tsx +33 -0
- package/src/react/components/progress.tsx +29 -0
- package/src/react/components/separator.tsx +14 -0
- package/src/react/components/slider.tsx +43 -0
- package/src/react/components/stack.tsx +55 -0
- package/src/react/components/switch.tsx +46 -0
- package/src/react/components/text.tsx +43 -0
- package/src/react/components/toggle-group.tsx +85 -0
- package/src/react/hooks/use-snap-accent.ts +45 -0
- package/src/react/index.tsx +321 -0
- package/src/react/lib/preview-primary-css.ts +57 -0
- package/src/react/lib/resolve-palette-hex.ts +20 -0
- package/src/schemas.ts +18 -644
- package/src/ui/badge.ts +13 -0
- package/src/ui/button.ts +9 -12
- package/src/ui/catalog.ts +106 -86
- package/src/ui/icon.ts +56 -0
- package/src/ui/image.ts +3 -2
- package/src/ui/index.ts +26 -29
- package/src/ui/input.ts +17 -0
- package/src/ui/item-group.ts +11 -0
- package/src/ui/item.ts +13 -0
- package/src/ui/progress.ts +25 -7
- package/src/ui/schema.ts +3 -3
- package/src/ui/separator.ts +9 -0
- package/src/ui/slider.ts +40 -10
- package/src/ui/stack.ts +9 -1
- package/src/ui/switch.ts +11 -0
- package/src/ui/text.ts +9 -4
- package/src/ui/toggle-group.ts +23 -0
- package/src/validator.ts +6 -176
- package/dist/ui/bar-chart.d.ts +0 -30
- package/dist/ui/bar-chart.js +0 -15
- package/dist/ui/button-group.d.ts +0 -19
- package/dist/ui/button-group.js +0 -18
- package/dist/ui/divider.d.ts +0 -3
- package/dist/ui/divider.js +0 -2
- package/dist/ui/grid.d.ts +0 -22
- package/dist/ui/grid.js +0 -16
- package/dist/ui/group.d.ts +0 -7
- package/dist/ui/group.js +0 -5
- package/dist/ui/list.d.ts +0 -13
- package/dist/ui/list.js +0 -13
- package/dist/ui/spacer.d.ts +0 -9
- package/dist/ui/spacer.js +0 -5
- package/dist/ui/text-input.d.ts +0 -7
- package/dist/ui/text-input.js +0 -12
- package/dist/ui/toggle.d.ts +0 -7
- package/dist/ui/toggle.js +0 -6
- package/src/ui/bar-chart.ts +0 -20
- package/src/ui/button-group.ts +0 -26
- package/src/ui/divider.ts +0 -5
- package/src/ui/grid.ts +0 -25
- package/src/ui/group.ts +0 -8
- package/src/ui/list.ts +0 -17
- package/src/ui/spacer.ts +0 -8
- package/src/ui/text-input.ts +0 -15
- package/src/ui/toggle.ts +0 -9
|
@@ -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,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,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,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,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,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,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
|
+
}
|