@farcaster/snap 1.13.0 → 1.15.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/react/components/action-button.js +18 -10
- package/dist/react/components/badge.js +8 -6
- package/dist/react/components/bar-chart.js +12 -17
- package/dist/react/components/cell-grid.js +16 -15
- package/dist/react/components/icon.js +4 -10
- package/dist/react/components/input.js +12 -6
- package/dist/react/components/item-group.js +3 -1
- package/dist/react/components/item.d.ts +3 -3
- package/dist/react/components/item.js +4 -3
- package/dist/react/components/progress.js +3 -3
- package/dist/react/components/separator.js +3 -1
- package/dist/react/components/slider.js +14 -10
- package/dist/react/components/switch.js +10 -12
- package/dist/react/components/text.js +5 -7
- package/dist/react/components/toggle-group.js +20 -6
- package/dist/react/hooks/use-snap-colors.d.ts +38 -0
- package/dist/react/hooks/use-snap-colors.js +82 -0
- package/dist/react/index.js +8 -5
- package/dist/react-native/components/snap-action-button.js +8 -18
- package/dist/react-native/components/snap-cell-grid.js +1 -1
- package/dist/react-native/components/snap-switch.js +1 -1
- package/dist/react-native/components/snap-toggle-group.js +8 -10
- package/dist/react-native/index.js +1 -1
- package/dist/react-native/theme.d.ts +6 -0
- package/dist/react-native/theme.js +12 -6
- package/dist/ui/catalog.d.ts +1 -0
- package/dist/ui/cell-grid.d.ts +1 -0
- package/dist/ui/cell-grid.js +1 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/react/components/action-button.tsx +24 -17
- package/src/react/components/badge.tsx +14 -17
- package/src/react/components/bar-chart.tsx +21 -19
- package/src/react/components/cell-grid.tsx +16 -19
- package/src/react/components/icon.tsx +5 -18
- package/src/react/components/input.tsx +20 -9
- package/src/react/components/item-group.tsx +4 -1
- package/src/react/components/item.tsx +13 -10
- package/src/react/components/progress.tsx +12 -7
- package/src/react/components/separator.tsx +8 -1
- package/src/react/components/slider.tsx +18 -15
- package/src/react/components/switch.tsx +12 -16
- package/src/react/components/text.tsx +11 -8
- package/src/react/components/toggle-group.tsx +26 -9
- package/src/react/hooks/use-snap-colors.ts +129 -0
- package/src/react/index.tsx +19 -18
- package/src/react-native/components/snap-action-button.tsx +8 -20
- package/src/react-native/components/snap-cell-grid.tsx +1 -1
- package/src/react-native/components/snap-switch.tsx +1 -1
- package/src/react-native/components/snap-toggle-group.tsx +8 -10
- package/src/react-native/index.tsx +1 -1
- package/src/react-native/theme.tsx +18 -6
- package/src/ui/cell-grid.ts +1 -0
- package/dist/react/hooks/use-snap-accent.d.ts +0 -13
- package/dist/react/hooks/use-snap-accent.js +0 -32
- package/src/react/hooks/use-snap-accent.ts +0 -45
|
@@ -1,20 +1,28 @@
|
|
|
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 { Button } from "@neynar/ui/button";
|
|
4
5
|
import { cn } from "@neynar/ui/utils";
|
|
5
|
-
import {
|
|
6
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
6
7
|
import { ICON_MAP } from "./icon.js";
|
|
7
|
-
const VARIANT_MAP = {
|
|
8
|
-
primary: "default",
|
|
9
|
-
secondary: "secondary",
|
|
10
|
-
};
|
|
11
8
|
export function SnapActionButton({ element: { props }, emit, }) {
|
|
12
9
|
const label = String(props.label ?? "Action");
|
|
13
|
-
const variant =
|
|
10
|
+
const variant = String(props.variant ?? "secondary");
|
|
11
|
+
const isPrimary = variant === "primary";
|
|
14
12
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
15
|
-
const
|
|
13
|
+
const colors = useSnapColors();
|
|
14
|
+
const [hovered, setHovered] = useState(false);
|
|
16
15
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const style = isPrimary
|
|
17
|
+
? {
|
|
18
|
+
backgroundColor: hovered ? colors.accentHover : colors.accent,
|
|
19
|
+
color: colors.accentFg,
|
|
20
|
+
borderColor: "transparent",
|
|
21
|
+
}
|
|
22
|
+
: {
|
|
23
|
+
backgroundColor: hovered ? `color-mix(in srgb, ${colors.accent} 15%, transparent)` : colors.muted,
|
|
24
|
+
color: colors.text,
|
|
25
|
+
borderColor: "transparent",
|
|
26
|
+
};
|
|
27
|
+
return (_jsx("div", { className: "w-full min-w-0 flex-1", children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("w-full gap-2"), style: style, onClick: () => emit("press"), onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label] }) }));
|
|
20
28
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Badge } from "@neynar/ui/badge";
|
|
4
|
-
import {
|
|
4
|
+
import { useSnapColors, pickForegroundForBg } from "../hooks/use-snap-colors.js";
|
|
5
5
|
import { ICON_MAP } from "./icon.js";
|
|
6
6
|
export function SnapBadge({ element: { props }, }) {
|
|
7
7
|
const content = String(props.label ?? "");
|
|
8
8
|
const variant = String(props.variant ?? "default");
|
|
9
9
|
const color = props.color ? String(props.color) : undefined;
|
|
10
10
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
11
|
-
const
|
|
12
|
-
const
|
|
11
|
+
const colors = useSnapColors();
|
|
12
|
+
const badgeColor = colors.colorHex(color);
|
|
13
|
+
const badgeFg = variant === "default" ? pickForegroundForBg(badgeColor) : badgeColor;
|
|
13
14
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const style = variant === "outline"
|
|
16
|
+
? { borderColor: badgeColor, color: badgeColor, backgroundColor: "transparent" }
|
|
17
|
+
: { backgroundColor: badgeColor, color: badgeFg, borderColor: "transparent" };
|
|
18
|
+
return (_jsxs(Badge, { variant: variant, className: "gap-1", style: style, children: [Icon && _jsx(Icon, { size: 12 }), content] }));
|
|
17
19
|
}
|
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
|
|
3
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
5
4
|
export function SnapBarChart({ element: { props }, }) {
|
|
6
|
-
const
|
|
5
|
+
const colors = useSnapColors();
|
|
7
6
|
const bars = Array.isArray(props.bars) ? props.bars : [];
|
|
8
|
-
const chartColor = String(props.color
|
|
7
|
+
const chartColor = props.color ? String(props.color) : undefined;
|
|
9
8
|
const maxVal = props.max != null
|
|
10
9
|
? Number(props.max)
|
|
11
10
|
: Math.max(...bars.map((b) => Number(b.value ?? 0)), 1);
|
|
12
|
-
function
|
|
13
|
-
if (bar.color
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
if (chartColor !== "accent" && chartColor in PALETTE_LIGHT_HEX) {
|
|
17
|
-
return `var(--snap-color-${chartColor}, ${PALETTE_LIGHT_HEX[chartColor]})`;
|
|
18
|
-
}
|
|
19
|
-
return "var(--primary)";
|
|
11
|
+
function barFill(bar) {
|
|
12
|
+
if (bar.color)
|
|
13
|
+
return colors.colorHex(bar.color);
|
|
14
|
+
return colors.colorHex(chartColor);
|
|
20
15
|
}
|
|
21
|
-
return (_jsx("div", { className: "flex w-full flex-col gap-2",
|
|
16
|
+
return (_jsx("div", { className: "flex w-full flex-col gap-2", children: bars.map((bar, i) => {
|
|
22
17
|
const value = Number(bar.value ?? 0);
|
|
23
18
|
const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
|
|
24
|
-
const fill =
|
|
25
|
-
return (_jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "
|
|
19
|
+
const fill = barFill(bar);
|
|
20
|
+
return (_jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "w-20 shrink-0 truncate text-right text-xs", style: { color: colors.textMuted }, children: String(bar.label ?? "") }), _jsx("div", { className: "h-2.5 flex-1 overflow-hidden rounded-full", style: { backgroundColor: colors.muted }, children: _jsx("div", { className: "h-full rounded-full transition-all", style: {
|
|
26
21
|
width: `${pct}%`,
|
|
27
22
|
minWidth: pct > 0 ? 4 : 0,
|
|
28
|
-
|
|
29
|
-
} }) }), _jsx("span", { className: "
|
|
23
|
+
backgroundColor: fill,
|
|
24
|
+
} }) }), _jsx("span", { className: "w-8 shrink-0 text-xs tabular-nums", style: { color: colors.textMuted }, children: value })] }, i));
|
|
30
25
|
}) }));
|
|
31
26
|
}
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useStateStore } from "@json-render/react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
|
-
import { POST_GRID_TAP_KEY
|
|
6
|
-
import {
|
|
7
|
-
import { useColorMode } from "@neynar/ui/color-mode";
|
|
5
|
+
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
6
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
8
7
|
export function SnapCellGrid({ element: { props }, }) {
|
|
9
8
|
const { get, set } = useStateStore();
|
|
10
|
-
const
|
|
11
|
-
const { mode: appearance } = useColorMode();
|
|
9
|
+
const colors = useSnapColors();
|
|
12
10
|
const cols = Number(props.cols ?? 2);
|
|
13
11
|
const rows = Number(props.rows ?? 2);
|
|
14
12
|
const select = String(props.select ?? "off");
|
|
@@ -18,6 +16,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
18
16
|
const gap = String(props.gap ?? "sm");
|
|
19
17
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
20
18
|
const gapPx = gapMap[gap] ?? 1;
|
|
19
|
+
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
21
20
|
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
22
21
|
const tapPath = `/inputs/${name}`;
|
|
23
22
|
const tapRaw = get(tapPath);
|
|
@@ -51,15 +50,12 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
51
50
|
content: c.content != null ? String(c.content) : undefined,
|
|
52
51
|
});
|
|
53
52
|
}
|
|
54
|
-
const ringColor = appearance === "dark" ? "#fff" : "#000";
|
|
55
53
|
const cellEls = [];
|
|
56
54
|
for (let r = 0; r < rows; r++) {
|
|
57
55
|
for (let c = 0; c < cols; c++) {
|
|
58
56
|
const cell = cellMap.get(`${r},${c}`);
|
|
59
57
|
const selected = interactive && isSelected(r, c);
|
|
60
|
-
const bg = cell?.color
|
|
61
|
-
? `var(--snap-color-${cell.color}, ${PALETTE_LIGHT_HEX[cell.color]})`
|
|
62
|
-
: "transparent";
|
|
58
|
+
const bg = cell?.color ? colors.colorHex(cell.color) : "transparent";
|
|
63
59
|
cellEls.push(_jsx("div", { role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, onClick: interactive ? () => handleTap(r, c) : undefined, onKeyDown: interactive
|
|
64
60
|
? (e) => {
|
|
65
61
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -67,11 +63,11 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
67
63
|
handleTap(r, c);
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
|
-
: undefined, className: cn("flex
|
|
66
|
+
: undefined, className: cn("flex items-center justify-center rounded text-xs font-semibold", interactive ? "cursor-pointer select-none" : "cursor-default"), style: {
|
|
67
|
+
height: rowHeight,
|
|
71
68
|
background: bg,
|
|
72
|
-
// Two-layer ring: 1px white/black inner + 2px accent outer
|
|
73
69
|
boxShadow: selected
|
|
74
|
-
? `inset 0 0 0 1px ${
|
|
70
|
+
? `inset 0 0 0 1px ${colors.mode === "dark" ? "#000" : "#fff"}, inset 0 0 0 2px ${colors.mode === "dark" ? "#fff" : "#000"}`
|
|
75
71
|
: undefined,
|
|
76
72
|
}, children: cell?.content ?? "" }, `${r}-${c}`));
|
|
77
73
|
}
|
|
@@ -79,8 +75,13 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
79
75
|
const selectionLabel = interactive && selectedSet.size > 0
|
|
80
76
|
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
81
77
|
: null;
|
|
82
|
-
return (_jsxs("div", {
|
|
83
|
-
|
|
78
|
+
return (_jsxs("div", { children: [_jsx("div", { style: {
|
|
79
|
+
display: "grid",
|
|
80
|
+
width: "100%",
|
|
81
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
84
82
|
gap: gapPx,
|
|
85
|
-
|
|
83
|
+
padding: 4,
|
|
84
|
+
borderRadius: 8,
|
|
85
|
+
backgroundColor: colors.muted,
|
|
86
|
+
}, children: cellEls }), selectionLabel && (_jsx("div", { className: "mt-1.5 truncate text-xs font-mono", style: { color: colors.textMuted }, children: selectionLabel }))] }));
|
|
86
87
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
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";
|
|
4
|
-
import {
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
5
5
|
export const ICON_MAP = {
|
|
6
6
|
"arrow-right": ArrowRight,
|
|
7
7
|
"arrow-left": ArrowLeft,
|
|
@@ -45,16 +45,10 @@ export function SnapIcon({ element: { props }, }) {
|
|
|
45
45
|
const name = String(props.name ?? "info");
|
|
46
46
|
const size = SIZE_PX[String(props.size ?? "md")] ?? 20;
|
|
47
47
|
const color = props.color ? String(props.color) : undefined;
|
|
48
|
-
const
|
|
48
|
+
const colors = useSnapColors();
|
|
49
49
|
const Icon = ICON_MAP[name];
|
|
50
50
|
if (!Icon)
|
|
51
51
|
return null;
|
|
52
|
-
const
|
|
53
|
-
return (_jsx("span", { style: {
|
|
54
|
-
display: "inline-flex",
|
|
55
|
-
alignItems: "center",
|
|
56
|
-
...(isAccent ? accentStyle : {}),
|
|
57
|
-
}, children: _jsx(Icon, { size: size, style: isAccent
|
|
58
|
-
? { color: "var(--snap-accent, currentColor)" }
|
|
59
|
-
: { color: `var(--snap-color-${color}, currentColor)` } }) }));
|
|
52
|
+
const iconColor = colors.colorHex(color);
|
|
53
|
+
return (_jsx("span", { style: { display: "inline-flex", alignItems: "center" }, children: _jsx(Icon, { size: size, style: { color: iconColor } }) }));
|
|
60
54
|
}
|
|
@@ -4,15 +4,21 @@ import { useId } from "react";
|
|
|
4
4
|
import { useStateStore } from "@json-render/react";
|
|
5
5
|
import { Input } from "@neynar/ui/input";
|
|
6
6
|
import { Label } from "@neynar/ui/label";
|
|
7
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
7
8
|
export function SnapInput({ element: { props }, }) {
|
|
8
|
-
const id = useId();
|
|
9
9
|
const { get, set } = useStateStore();
|
|
10
|
+
const colors = useSnapColors();
|
|
10
11
|
const name = String(props.name ?? "input");
|
|
11
|
-
const
|
|
12
|
+
const type = String(props.type ?? "text");
|
|
12
13
|
const label = props.label ? String(props.label) : undefined;
|
|
13
14
|
const placeholder = props.placeholder ? String(props.placeholder) : undefined;
|
|
14
|
-
const maxLength =
|
|
15
|
-
const
|
|
16
|
-
const value =
|
|
17
|
-
|
|
15
|
+
const maxLength = props.maxLength ? Number(props.maxLength) : undefined;
|
|
16
|
+
const path = `/inputs/${name}`;
|
|
17
|
+
const value = get(path) ?? (props.defaultValue != null ? String(props.defaultValue) : "");
|
|
18
|
+
const id = useId();
|
|
19
|
+
return (_jsxs("div", { className: "w-full space-y-1.5", children: [label && (_jsx(Label, { htmlFor: id, style: { color: colors.text }, children: label })), _jsx(Input, { id: id, type: type === "number" ? "number" : "text", placeholder: placeholder, maxLength: maxLength, value: value, onChange: (e) => set(path, e.target.value), style: {
|
|
20
|
+
backgroundColor: colors.inputBg,
|
|
21
|
+
borderColor: colors.inputBorder,
|
|
22
|
+
color: colors.text,
|
|
23
|
+
} })] }));
|
|
18
24
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Children, Fragment } from "react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
5
6
|
const GAP_MAP = {
|
|
6
7
|
none: "gap-0",
|
|
7
8
|
sm: "gap-1",
|
|
@@ -13,5 +14,6 @@ export function SnapItemGroup({ element: { props }, children, }) {
|
|
|
13
14
|
const separator = Boolean(props.separator);
|
|
14
15
|
const gap = GAP_MAP[String(props.gap ?? "sm")] ?? "gap-1";
|
|
15
16
|
const items = Children.toArray(children);
|
|
16
|
-
|
|
17
|
+
const colors = useSnapColors();
|
|
18
|
+
return (_jsx("div", { className: cn("flex flex-col", border && "rounded-lg border", gap), style: border ? { borderColor: colors.border } : undefined, children: items.map((child, i) => (_jsxs(Fragment, { children: [separator && i > 0 && (_jsx("div", { className: "h-px", style: { backgroundColor: colors.border } })), child] }, i))) }));
|
|
17
19
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function SnapItem({ element: { props }, children, }: {
|
|
1
|
+
export declare function SnapItem({ element: { props, children: childIds }, children, }: {
|
|
3
2
|
element: {
|
|
4
3
|
props: Record<string, unknown>;
|
|
4
|
+
children?: string[];
|
|
5
5
|
};
|
|
6
|
-
children?: ReactNode;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Item, ItemContent, ItemTitle, ItemDescription, ItemActions, } from "@neynar/ui/item";
|
|
4
|
-
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
5
|
+
export function SnapItem({ element: { props, children: childIds }, children, }) {
|
|
5
6
|
const title = String(props.title ?? "");
|
|
6
7
|
const description = props.description ? String(props.description) : undefined;
|
|
7
|
-
const
|
|
8
|
-
return (_jsxs(Item, {
|
|
8
|
+
const colors = useSnapColors();
|
|
9
|
+
return (_jsxs(Item, { className: "flex-1 py-1.5 px-2.5", children: [_jsxs(ItemContent, { className: "gap-0.5", children: [_jsx(ItemTitle, { style: { color: colors.text }, children: title }), description && (_jsx(ItemDescription, { className: "mt-0", style: { color: colors.textMuted }, children: description }))] }), childIds && childIds.length > 0 && _jsx(ItemActions, { children: children })] }));
|
|
9
10
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
4
4
|
export function SnapProgress({ element: { props }, }) {
|
|
5
|
-
const
|
|
5
|
+
const colors = useSnapColors();
|
|
6
6
|
const value = Number(props.value ?? 0);
|
|
7
7
|
const max = Math.max(1, Number(props.max ?? 100));
|
|
8
8
|
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
9
9
|
const label = props.label ? String(props.label) : null;
|
|
10
|
-
return (_jsxs("div", { className: "flex w-full flex-1 flex-col gap-1",
|
|
10
|
+
return (_jsxs("div", { className: "flex w-full flex-1 flex-col gap-1", children: [label && (_jsx("span", { className: "text-xs", style: { color: colors.textMuted }, children: label })), _jsx("div", { className: "h-2.5 w-full overflow-hidden rounded-full", style: { backgroundColor: colors.muted }, children: _jsx("div", { className: "h-full rounded-full transition-all", style: { width: `${percent}%`, backgroundColor: colors.accent } }) })] }));
|
|
11
11
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Separator } from "@neynar/ui/separator";
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
4
5
|
export function SnapSeparator({ element: { props }, }) {
|
|
5
6
|
const orientation = props.orientation ?? "horizontal";
|
|
6
|
-
|
|
7
|
+
const colors = useSnapColors();
|
|
8
|
+
return (_jsx(Separator, { orientation: orientation, style: { backgroundColor: colors.border } }));
|
|
7
9
|
}
|
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useStateStore } from "@json-render/react";
|
|
4
4
|
import { Label } from "@neynar/ui/label";
|
|
5
|
-
import {
|
|
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.
|
|
5
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
8
6
|
export function SnapSlider({ element: { props }, }) {
|
|
9
7
|
const { get, set } = useStateStore();
|
|
10
|
-
const
|
|
8
|
+
const colors = useSnapColors();
|
|
11
9
|
const name = String(props.name ?? "slider");
|
|
12
|
-
const path = `/inputs/${name}`;
|
|
13
|
-
const label = props.label ? String(props.label) : undefined;
|
|
14
10
|
const min = Number(props.min ?? 0);
|
|
15
11
|
const max = Number(props.max ?? 100);
|
|
16
|
-
const step =
|
|
17
|
-
const
|
|
12
|
+
const step = Number(props.step ?? 1);
|
|
13
|
+
const label = props.label ? String(props.label) : undefined;
|
|
14
|
+
const path = `/inputs/${name}`;
|
|
18
15
|
const raw = get(path);
|
|
19
|
-
const value = raw
|
|
20
|
-
|
|
16
|
+
const value = raw !== undefined
|
|
17
|
+
? Number(raw)
|
|
18
|
+
: props.defaultValue !== undefined
|
|
19
|
+
? Number(props.defaultValue)
|
|
20
|
+
: (min + max) / 2;
|
|
21
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-1.5", children: [label && _jsx(Label, { style: { color: colors.text }, 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 cursor-pointer", style: {
|
|
22
|
+
backgroundColor: colors.muted,
|
|
23
|
+
accentColor: colors.accent,
|
|
24
|
+
} })] }));
|
|
21
25
|
}
|
|
@@ -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 {
|
|
8
|
-
import {
|
|
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
|
|
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
|
|
13
|
+
const path = `/inputs/${name}`;
|
|
19
14
|
const raw = get(path);
|
|
20
|
-
const checked = raw
|
|
21
|
-
|
|
22
|
-
|
|
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,13 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Text } from "@neynar/ui/typography";
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
4
5
|
const SIZE_MAP = {
|
|
5
|
-
md: {
|
|
6
|
-
sm: {
|
|
7
|
-
};
|
|
8
|
-
const WEIGHT_MAP = {
|
|
9
|
-
bold: "bold",
|
|
10
|
-
normal: "normal",
|
|
6
|
+
md: { textSize: "base" },
|
|
7
|
+
sm: { textSize: "sm" },
|
|
11
8
|
};
|
|
12
9
|
export function SnapText({ element: { props }, }) {
|
|
13
10
|
const content = String(props.content ?? "");
|
|
@@ -15,5 +12,6 @@ export function SnapText({ element: { props }, }) {
|
|
|
15
12
|
const weight = props.weight ? String(props.weight) : undefined;
|
|
16
13
|
const align = props.align ?? undefined;
|
|
17
14
|
const config = SIZE_MAP[size] ?? SIZE_MAP.md;
|
|
18
|
-
|
|
15
|
+
const colors = useSnapColors();
|
|
16
|
+
return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: "flex-1", style: { color: colors.text }, children: content }));
|
|
19
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 {
|
|
7
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
7
8
|
export function SnapToggleGroup({ element: { props }, }) {
|
|
8
9
|
const { get, set } = useStateStore();
|
|
9
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
: "
|
|
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,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useStateStore } from "@json-render/react";
|
|
4
|
+
import { useColorMode } from "@neynar/ui/color-mode";
|
|
5
|
+
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
|
|
6
|
+
import { useSnapPreviewPageAccent } from "../accent-context.js";
|
|
7
|
+
import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX } from "@farcaster/snap";
|
|
8
|
+
/** Readable foreground color (black or white) for a given hex background. */
|
|
9
|
+
export function pickForegroundForBg(hex) {
|
|
10
|
+
const h = hex.replace(/^#/, "");
|
|
11
|
+
if (h.length !== 6)
|
|
12
|
+
return "#ffffff";
|
|
13
|
+
const r = Number.parseInt(h.slice(0, 2), 16);
|
|
14
|
+
const g = Number.parseInt(h.slice(2, 4), 16);
|
|
15
|
+
const b = Number.parseInt(h.slice(4, 6), 16);
|
|
16
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
17
|
+
return yiq >= 180 ? "#0a0a0a" : "#ffffff";
|
|
18
|
+
}
|
|
19
|
+
const NEUTRAL_LIGHT = {
|
|
20
|
+
text: "#111111",
|
|
21
|
+
textMuted: "#6B7280",
|
|
22
|
+
border: "#E5E7EB",
|
|
23
|
+
muted: "rgba(0,0,0,0.06)",
|
|
24
|
+
surface: "#ffffff",
|
|
25
|
+
inputBorder: "#E5E7EB",
|
|
26
|
+
inputBg: "rgba(0,0,0,0.06)",
|
|
27
|
+
};
|
|
28
|
+
const NEUTRAL_DARK = {
|
|
29
|
+
text: "#FAFAFA",
|
|
30
|
+
textMuted: "#A1A1AA",
|
|
31
|
+
border: "#2D2D44",
|
|
32
|
+
muted: "rgba(255,255,255,0.03)",
|
|
33
|
+
surface: "#23262f",
|
|
34
|
+
inputBorder: "#3F3F46",
|
|
35
|
+
inputBg: "rgba(255,255,255,0.03)",
|
|
36
|
+
};
|
|
37
|
+
function buildSnapColors(accentName, mode) {
|
|
38
|
+
const accent = resolveSnapPaletteHex(accentName, mode);
|
|
39
|
+
const accentFg = pickForegroundForBg(accent);
|
|
40
|
+
const neutrals = mode === "dark" ? NEUTRAL_DARK : NEUTRAL_LIGHT;
|
|
41
|
+
const paletteMap = mode === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
|
|
42
|
+
const accentHover = mode === "light"
|
|
43
|
+
? `color-mix(in srgb, ${accent} 82%, #000000)`
|
|
44
|
+
: `color-mix(in srgb, ${accent} 78%, #ffffff)`;
|
|
45
|
+
const outlineHover = `color-mix(in srgb, ${accent} 14%, ${neutrals.surface})`;
|
|
46
|
+
const paletteHex = (name) => resolveSnapPaletteHex(name, mode);
|
|
47
|
+
const colorHex = (name) => {
|
|
48
|
+
if (!name || name === "accent")
|
|
49
|
+
return accent;
|
|
50
|
+
if (Object.hasOwn(paletteMap, name))
|
|
51
|
+
return paletteMap[name];
|
|
52
|
+
return accent;
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
accent,
|
|
56
|
+
accentFg,
|
|
57
|
+
accentHover,
|
|
58
|
+
outlineHover,
|
|
59
|
+
...neutrals,
|
|
60
|
+
mode,
|
|
61
|
+
paletteHex,
|
|
62
|
+
colorHex,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns fully resolved color values for snap components.
|
|
67
|
+
* All colors are concrete hex values (or color-mix expressions for hover states)
|
|
68
|
+
* so they can be used as inline styles, independent of host app CSS.
|
|
69
|
+
*/
|
|
70
|
+
export function useSnapColors() {
|
|
71
|
+
const { get } = useStateStore();
|
|
72
|
+
const { mode } = useColorMode();
|
|
73
|
+
const pageAccent = useSnapPreviewPageAccent();
|
|
74
|
+
const fromState = get("/theme/accent");
|
|
75
|
+
const accentRaw = (typeof pageAccent === "string" && pageAccent.length > 0
|
|
76
|
+
? pageAccent
|
|
77
|
+
: fromState) ?? undefined;
|
|
78
|
+
const accentName = typeof accentRaw === "string" && accentRaw.length > 0
|
|
79
|
+
? accentRaw
|
|
80
|
+
: "purple";
|
|
81
|
+
return useMemo(() => buildSnapColors(accentName, mode), [accentName, mode]);
|
|
82
|
+
}
|
package/dist/react/index.js
CHANGED
|
@@ -174,7 +174,7 @@ export function SnapView({ snap, handlers, loading = false, appearance = "dark",
|
|
|
174
174
|
break;
|
|
175
175
|
}
|
|
176
176
|
}, [handlers]);
|
|
177
|
-
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}),
|
|
177
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}), _jsx("div", { style: {
|
|
178
178
|
position: "absolute",
|
|
179
179
|
inset: 0,
|
|
180
180
|
display: "flex",
|
|
@@ -182,10 +182,13 @@ export function SnapView({ snap, handlers, loading = false, appearance = "dark",
|
|
|
182
182
|
justifyContent: "center",
|
|
183
183
|
zIndex: 10,
|
|
184
184
|
fontSize: 14,
|
|
185
|
-
color: "
|
|
186
|
-
background: "
|
|
187
|
-
backdropFilter: "blur(
|
|
188
|
-
|
|
185
|
+
color: appearance === "dark" ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.4)",
|
|
186
|
+
background: appearance === "dark" ? "rgba(0,0,0,0.3)" : "rgba(255,255,255,0.5)",
|
|
187
|
+
backdropFilter: loading ? "blur(8px)" : "blur(0px)",
|
|
188
|
+
opacity: loading ? 1 : 0,
|
|
189
|
+
pointerEvents: loading ? "auto" : "none",
|
|
190
|
+
transition: "opacity 0.3s ease, backdrop-filter 0.3s ease",
|
|
191
|
+
}, 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
192
|
applyStatePaths(stateRef.current, changes);
|
|
190
193
|
}, onAction: handleAction }, pageKey) }) })] }));
|
|
191
194
|
}
|