@farcaster/snap 1.10.0 → 1.14.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 (79) hide show
  1. package/dist/constants.d.ts +8 -0
  2. package/dist/constants.js +10 -0
  3. package/dist/index.d.ts +1 -2
  4. package/dist/index.js +1 -2
  5. package/dist/react/catalog-renderer.js +4 -0
  6. package/dist/react/components/action-button.js +18 -10
  7. package/dist/react/components/badge.js +8 -6
  8. package/dist/react/components/bar-chart.d.ts +5 -0
  9. package/dist/react/components/bar-chart.js +26 -0
  10. package/dist/react/components/cell-grid.d.ts +5 -0
  11. package/dist/react/components/cell-grid.js +82 -0
  12. package/dist/react/components/icon.js +4 -10
  13. package/dist/react/components/input.js +12 -6
  14. package/dist/react/components/item-group.js +3 -1
  15. package/dist/react/components/item.d.ts +3 -3
  16. package/dist/react/components/item.js +4 -3
  17. package/dist/react/components/progress.js +3 -3
  18. package/dist/react/components/separator.js +3 -1
  19. package/dist/react/components/slider.js +14 -10
  20. package/dist/react/components/switch.js +10 -12
  21. package/dist/react/components/text.js +5 -7
  22. package/dist/react/components/toggle-group.js +20 -6
  23. package/dist/react/hooks/use-snap-colors.d.ts +38 -0
  24. package/dist/react/hooks/use-snap-colors.js +82 -0
  25. package/dist/react-native/catalog-renderer.js +4 -0
  26. package/dist/react-native/components/snap-action-button.js +8 -18
  27. package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
  28. package/dist/react-native/components/snap-bar-chart.js +39 -0
  29. package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
  30. package/dist/react-native/components/snap-cell-grid.js +96 -0
  31. package/dist/react-native/components/snap-switch.js +1 -1
  32. package/dist/react-native/components/snap-toggle-group.js +8 -10
  33. package/dist/react-native/theme.d.ts +6 -0
  34. package/dist/react-native/theme.js +12 -6
  35. package/dist/ui/bar-chart.d.ts +30 -0
  36. package/dist/ui/bar-chart.js +30 -0
  37. package/dist/ui/catalog.d.ts +65 -0
  38. package/dist/ui/catalog.js +10 -0
  39. package/dist/ui/cell-grid.d.ts +33 -0
  40. package/dist/ui/cell-grid.js +38 -0
  41. package/dist/ui/index.d.ts +4 -0
  42. package/dist/ui/index.js +2 -0
  43. package/llms.txt +16 -1
  44. package/package.json +1 -1
  45. package/src/constants.ts +12 -0
  46. package/src/index.ts +6 -2
  47. package/src/react/catalog-renderer.tsx +4 -0
  48. package/src/react/components/action-button.tsx +24 -17
  49. package/src/react/components/badge.tsx +14 -17
  50. package/src/react/components/bar-chart.tsx +69 -0
  51. package/src/react/components/cell-grid.tsx +124 -0
  52. package/src/react/components/icon.tsx +5 -18
  53. package/src/react/components/input.tsx +20 -9
  54. package/src/react/components/item-group.tsx +4 -1
  55. package/src/react/components/item.tsx +13 -10
  56. package/src/react/components/progress.tsx +12 -7
  57. package/src/react/components/separator.tsx +8 -1
  58. package/src/react/components/slider.tsx +18 -15
  59. package/src/react/components/switch.tsx +12 -16
  60. package/src/react/components/text.tsx +11 -8
  61. package/src/react/components/toggle-group.tsx +26 -9
  62. package/src/react/hooks/use-snap-colors.ts +129 -0
  63. package/src/react-native/catalog-renderer.tsx +4 -0
  64. package/src/react-native/components/snap-action-button.tsx +8 -20
  65. package/src/react-native/components/snap-bar-chart.tsx +73 -0
  66. package/src/react-native/components/snap-cell-grid.tsx +152 -0
  67. package/src/react-native/components/snap-switch.tsx +1 -1
  68. package/src/react-native/components/snap-toggle-group.tsx +8 -10
  69. package/src/react-native/theme.tsx +18 -6
  70. package/src/ui/bar-chart.ts +38 -0
  71. package/src/ui/catalog.ts +12 -0
  72. package/src/ui/cell-grid.ts +48 -0
  73. package/src/ui/index.ts +6 -0
  74. package/dist/middleware.d.ts +0 -3
  75. package/dist/middleware.js +0 -3
  76. package/dist/react/hooks/use-snap-accent.d.ts +0 -13
  77. package/dist/react/hooks/use-snap-accent.js +0 -32
  78. package/src/middleware.ts +0 -7
  79. package/src/react/hooks/use-snap-accent.ts +0 -45
@@ -1,3 +1,11 @@
1
1
  export declare const SPEC_VERSION: "1.0";
2
2
  export declare const MEDIA_TYPE: "application/vnd.farcaster.snap+json";
3
3
  export declare const EFFECT_VALUES: readonly ["confetti"];
4
+ export declare const POST_GRID_TAP_KEY: "grid_tap";
5
+ export declare const GRID_MIN_COLS = 2;
6
+ export declare const GRID_MAX_COLS = 32;
7
+ export declare const GRID_MIN_ROWS = 2;
8
+ export declare const GRID_MAX_ROWS = 16;
9
+ export declare const GRID_GAP_VALUES: readonly ["none", "sm", "md", "lg"];
10
+ export declare const BAR_CHART_MAX_BARS = 6;
11
+ export declare const BAR_CHART_LABEL_MAX_CHARS = 40;
package/dist/constants.js CHANGED
@@ -1,3 +1,13 @@
1
1
  export const SPEC_VERSION = "1.0";
2
2
  export const MEDIA_TYPE = "application/vnd.farcaster.snap+json";
3
3
  export const EFFECT_VALUES = ["confetti"];
4
+ // ─── Pixel grid ────────────────────────────────────────
5
+ export const POST_GRID_TAP_KEY = "grid_tap";
6
+ export const GRID_MIN_COLS = 2;
7
+ export const GRID_MAX_COLS = 32;
8
+ export const GRID_MIN_ROWS = 2;
9
+ export const GRID_MAX_ROWS = 16;
10
+ export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"];
11
+ // ─── Bar chart ─────────────────────────────────────────
12
+ export const BAR_CHART_MAX_BARS = 6;
13
+ export const BAR_CHART_LABEL_MAX_CHARS = 40;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export type { Spec as SnapSpec, UIElement as SnapUIElement, } from "@json-render/core";
2
- export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES } from "./constants.js";
2
+ export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, } from "./constants.js";
3
3
  export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, type PaletteColor, } from "./colors.js";
4
4
  export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, type SnapAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapElementInput, type SnapSpecInput, type SnapFunction, type SnapPayload, } from "./schemas.js";
5
5
  export { validateSnapResponse, type ValidationResult } from "./validator.js";
6
- export { type Middleware, useMiddleware } from "./middleware.js";
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
- export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES } from "./constants.js";
1
+ export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, } from "./constants.js";
2
2
  export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "./colors.js";
3
3
  export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, } from "./schemas.js";
4
4
  export { validateSnapResponse } from "./validator.js";
5
- export { useMiddleware } from "./middleware.js";
@@ -15,6 +15,8 @@ import { SnapStack } from "./components/stack.js";
15
15
  import { SnapSwitch } from "./components/switch.js";
16
16
  import { SnapText } from "./components/text.js";
17
17
  import { SnapToggleGroup } from "./components/toggle-group.js";
18
+ import { SnapBarChart } from "./components/bar-chart.js";
19
+ import { SnapCellGrid } from "./components/cell-grid.js";
18
20
  /**
19
21
  * Maps snap json-render catalog types to React components.
20
22
  * Keys match the snap wire-format `type` strings exactly.
@@ -34,4 +36,6 @@ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
34
36
  switch: SnapSwitch,
35
37
  text: SnapText,
36
38
  toggle_group: SnapToggleGroup,
39
+ bar_chart: SnapBarChart,
40
+ cell_grid: SnapCellGrid,
37
41
  });
@@ -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 { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
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 = VARIANT_MAP[String(props.variant ?? "secondary")] ?? "secondary";
10
+ const variant = String(props.variant ?? "secondary");
11
+ const isPrimary = variant === "primary";
14
12
  const iconName = props.icon ? String(props.icon) : undefined;
15
- const accentStyle = useSnapAccentScopeStyle();
13
+ const colors = useSnapColors();
14
+ const [hovered, setHovered] = useState(false);
16
15
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
17
- return (_jsx("div", { className: "w-full min-w-0 flex-1", style: accentStyle, children: _jsxs(Button, { type: "button", variant: variant, className: cn("w-full gap-2", variant === "default" &&
18
- "hover:!bg-[var(--snap-action-primary-hover)]", variant !== "default" &&
19
- "hover:!bg-[var(--snap-action-outline-hover)]"), onClick: () => emit("press"), children: [Icon && _jsx(Icon, { size: 16 }), label] }) }));
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 { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
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 accentStyle = useSnapAccentScopeStyle();
12
- const isAccent = !color || color === "accent";
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
- return (_jsx("span", { style: isAccent ? accentStyle : undefined, children: _jsxs(Badge, { variant: variant, className: "gap-1", style: variant === "outline" && !isAccent
15
- ? { borderColor: `var(--snap-color-${color})`, color: `var(--snap-color-${color})` }
16
- : undefined, children: [Icon && _jsx(Icon, { size: 12 }), content] }) }));
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
  }
@@ -0,0 +1,5 @@
1
+ export declare function SnapBarChart({ element: { props }, }: {
2
+ element: {
3
+ props: Record<string, unknown>;
4
+ };
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
4
+ export function SnapBarChart({ element: { props }, }) {
5
+ const colors = useSnapColors();
6
+ const bars = Array.isArray(props.bars) ? props.bars : [];
7
+ const chartColor = props.color ? String(props.color) : undefined;
8
+ const maxVal = props.max != null
9
+ ? Number(props.max)
10
+ : Math.max(...bars.map((b) => Number(b.value ?? 0)), 1);
11
+ function barFill(bar) {
12
+ if (bar.color)
13
+ return colors.colorHex(bar.color);
14
+ return colors.colorHex(chartColor);
15
+ }
16
+ return (_jsx("div", { className: "flex w-full flex-col gap-2", children: bars.map((bar, i) => {
17
+ const value = Number(bar.value ?? 0);
18
+ const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
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: {
21
+ width: `${pct}%`,
22
+ minWidth: pct > 0 ? 4 : 0,
23
+ backgroundColor: fill,
24
+ } }) }), _jsx("span", { className: "w-8 shrink-0 text-xs tabular-nums", style: { color: colors.textMuted }, children: value })] }, i));
25
+ }) }));
26
+ }
@@ -0,0 +1,5 @@
1
+ export declare function SnapCellGrid({ element: { props }, }: {
2
+ element: {
3
+ props: Record<string, unknown>;
4
+ };
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useStateStore } from "@json-render/react";
4
+ import { cn } from "@neynar/ui/utils";
5
+ import { POST_GRID_TAP_KEY } from "@farcaster/snap";
6
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
7
+ export function SnapCellGrid({ element: { props }, }) {
8
+ const { get, set } = useStateStore();
9
+ const colors = useSnapColors();
10
+ const cols = Number(props.cols ?? 2);
11
+ const rows = Number(props.rows ?? 2);
12
+ const select = String(props.select ?? "off");
13
+ const interactive = select !== "off";
14
+ const isMultiple = select === "multiple";
15
+ const cells = Array.isArray(props.cells) ? props.cells : [];
16
+ const gap = String(props.gap ?? "sm");
17
+ const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
18
+ const gapPx = gapMap[gap] ?? 1;
19
+ const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
20
+ const tapPath = `/inputs/${name}`;
21
+ const tapRaw = get(tapPath);
22
+ // Parse selection — single mode: "row,col" string; multi mode: "row,col|row,col|..." string
23
+ const selectedSet = new Set();
24
+ if (typeof tapRaw === "string" && tapRaw.length > 0) {
25
+ for (const part of tapRaw.split("|")) {
26
+ if (part.includes(","))
27
+ selectedSet.add(part);
28
+ }
29
+ }
30
+ const isSelected = (r, c) => selectedSet.has(`${r},${c}`);
31
+ const handleTap = (r, c) => {
32
+ const key = `${r},${c}`;
33
+ if (isMultiple) {
34
+ const next = new Set(selectedSet);
35
+ if (next.has(key))
36
+ next.delete(key);
37
+ else
38
+ next.add(key);
39
+ set(tapPath, [...next].join("|"));
40
+ }
41
+ else {
42
+ set(tapPath, key);
43
+ }
44
+ };
45
+ const cellMap = new Map();
46
+ for (const c of cells) {
47
+ cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
48
+ color: c.color,
49
+ content: c.content != null ? String(c.content) : undefined,
50
+ });
51
+ }
52
+ const cellEls = [];
53
+ for (let r = 0; r < rows; r++) {
54
+ for (let c = 0; c < cols; c++) {
55
+ const cell = cellMap.get(`${r},${c}`);
56
+ const selected = interactive && isSelected(r, c);
57
+ const bg = cell?.color ? colors.colorHex(cell.color) : "transparent";
58
+ cellEls.push(_jsx("div", { role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, onClick: interactive ? () => handleTap(r, c) : undefined, onKeyDown: interactive
59
+ ? (e) => {
60
+ if (e.key === "Enter" || e.key === " ") {
61
+ e.preventDefault();
62
+ handleTap(r, c);
63
+ }
64
+ }
65
+ : undefined, className: cn("flex min-h-7 items-center justify-center rounded text-xs font-semibold", interactive ? "cursor-pointer select-none" : "cursor-default"), style: {
66
+ background: bg,
67
+ // Two-layer ring: 1px white/black inner + 2px accent outer
68
+ boxShadow: selected
69
+ ? `inset 0 0 0 1px ${colors.mode === "dark" ? "#000" : "#fff"}, inset 0 0 0 2px ${colors.mode === "dark" ? "#fff" : "#000"}`
70
+ : undefined,
71
+ }, children: cell?.content ?? "" }, `${r}-${c}`));
72
+ }
73
+ }
74
+ const selectionLabel = interactive && selectedSet.size > 0
75
+ ? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
76
+ : null;
77
+ return (_jsxs("div", { children: [_jsx("div", { className: "grid w-full rounded-lg p-1", style: {
78
+ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
79
+ gap: gapPx,
80
+ backgroundColor: colors.muted,
81
+ }, children: cellEls }), selectionLabel && (_jsx("div", { className: "mt-1.5 truncate text-xs font-mono", style: { color: colors.textMuted }, children: selectionLabel }))] }));
82
+ }
@@ -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 { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
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 accentStyle = useSnapAccentScopeStyle();
48
+ const colors = useSnapColors();
49
49
  const Icon = ICON_MAP[name];
50
50
  if (!Icon)
51
51
  return null;
52
- const isAccent = !color || color === "accent";
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 path = `/inputs/${name}`;
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 = typeof props.maxLength === "number" ? props.maxLength : undefined;
15
- const raw = get(path);
16
- const value = typeof raw === "string" ? raw : "";
17
- return (_jsxs("div", { className: "w-full space-y-1.5", children: [label && _jsx(Label, { htmlFor: id, children: label }), _jsx(Input, { id: id, value: value, onChange: (e) => set(path, e.target.value), placeholder: placeholder, maxLength: maxLength })] }));
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
- return (_jsx("div", { className: cn("flex flex-col", border && "rounded-lg border", gap), children: items.map((child, i) => (_jsxs(Fragment, { children: [separator && i > 0 && (_jsx("div", { className: "h-px bg-border" })), child] }, i))) }));
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
- import type { ReactNode } from "react";
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
- export function SnapItem({ element: { props }, children, }) {
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 variant = props.variant ?? "default";
8
- return (_jsxs(Item, { variant: variant, className: "flex-1 py-1.5 px-2.5", children: [_jsxs(ItemContent, { className: "gap-0.5", children: [_jsx(ItemTitle, { children: title }), description && _jsx(ItemDescription, { className: "mt-0", children: description })] }), children && _jsx(ItemActions, { children: children })] }));
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 { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
3
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
4
4
  export function SnapProgress({ element: { props }, }) {
5
- const accentStyle = useSnapAccentScopeStyle();
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", style: accentStyle, children: [label && (_jsx("span", { className: "text-muted-foreground text-xs", children: label })), _jsx("div", { className: "bg-muted h-2.5 w-full overflow-hidden rounded-full", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${percent}%` } }) })] }));
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
- return _jsx(Separator, { orientation: orientation });
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 { 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.
5
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
8
6
  export function SnapSlider({ element: { props }, }) {
9
7
  const { get, set } = useStateStore();
10
- const accentStyle = useSnapAccentScopeStyle();
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 = props.step != null ? Number(props.step) : 1;
17
- const fallback = props.defaultValue != null ? Number(props.defaultValue) : (min + max) / 2;
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 === 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)" } })] }));
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 { useColorMode } from "@neynar/ui/color-mode";
8
- import { cn } from "@neynar/ui/utils";
9
- import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
6
+ import { Label } from "@neynar/ui/label";
7
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
10
8
  export function SnapSwitch({ element: { props }, }) {
11
- const id = useId();
12
9
  const { get, set } = useStateStore();
13
- const { mode } = useColorMode();
14
- const accentStyle = useSnapAccentScopeStyle();
10
+ const colors = useSnapColors();
15
11
  const name = String(props.name ?? "switch");
16
- const path = `/inputs/${name}`;
17
12
  const label = props.label ? String(props.label) : undefined;
18
- const fallback = Boolean(props.defaultChecked ?? false);
13
+ const path = `/inputs/${name}`;
19
14
  const raw = get(path);
20
- const checked = raw === undefined || raw === null ? fallback : Boolean(raw);
21
- return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [label && (_jsx(Label, { htmlFor: id, className: "text-foreground font-normal", children: label })), _jsx(Switch, { id: id, checked: checked, onCheckedChange: (v) => set(path, v), style: accentStyle, className: cn(mode === "light" &&
22
- "data-unchecked:!bg-border data-unchecked:!border-(--input-border)") })] }));
15
+ const checked = raw !== undefined ? Boolean(raw) : Boolean(props.defaultChecked);
16
+ const id = useId();
17
+ return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [label && (_jsx(Label, { htmlFor: id, className: "font-normal", style: { color: colors.text }, children: label })), _jsx(Switch, { id: id, checked: checked, onCheckedChange: (v) => set(path, v), style: {
18
+ backgroundColor: checked ? colors.accent : colors.muted,
19
+ borderColor: checked ? colors.accent : colors.inputBorder,
20
+ } })] }));
23
21
  }
@@ -1,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: { component: "text", textSize: "base" },
6
- sm: { component: "text", textSize: "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
- return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: "flex-1", children: content }));
15
+ const colors = useSnapColors();
16
+ return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: "flex-1", style: { color: colors.text }, children: content }));
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 { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
7
+ import { useSnapColors } from "../hooks/use-snap-colors.js";
7
8
  export function SnapToggleGroup({ element: { props }, }) {
8
9
  const { get, set } = useStateStore();
9
- const accentStyle = useSnapAccentScopeStyle();
10
+ const colors = useSnapColors();
10
11
  const name = String(props.name ?? "toggle_group");
11
12
  const path = `/inputs/${name}`;
12
13
  const label = props.label ? String(props.label) : undefined;
@@ -43,10 +44,23 @@ export function SnapToggleGroup({ element: { props }, }) {
43
44
  }
44
45
  };
45
46
  const isVertical = orientation === "vertical";
46
- return (_jsxs("div", { className: "w-full space-y-1.5", style: accentStyle, children: [label && _jsx(Label, { children: label }), _jsx("div", { className: cn("flex gap-1 rounded-lg bg-border/20 p-1", isVertical ? "flex-col" : "flex-row"), children: options.map((opt) => {
47
+ const [hoveredIdx, setHoveredIdx] = useState(null);
48
+ return (_jsxs("div", { className: "w-full space-y-1.5", children: [label && _jsx(Label, { style: { color: colors.text }, children: label }), _jsx("div", { className: cn("flex gap-1 rounded-lg p-1", isVertical ? "flex-col" : "flex-row"), style: { backgroundColor: colors.muted }, children: options.map((opt, i) => {
47
49
  const isSelected = selected.includes(opt);
48
- return (_jsx("button", { type: "button", onClick: () => toggle(opt), className: cn("rounded-md px-3 py-2 text-sm font-medium transition-colors", isVertical ? "w-full" : "flex-1", isSelected
49
- ? "bg-primary text-primary-foreground"
50
- : "text-foreground hover:bg-border/30"), children: opt }, opt));
50
+ const isHovered = hoveredIdx === i && !isSelected;
51
+ return (_jsx("button", { type: "button", onClick: () => toggle(opt), onPointerEnter: () => setHoveredIdx(i), onPointerLeave: () => setHoveredIdx(null), className: cn("rounded-md px-3 py-2 text-sm font-medium transition-colors", isVertical ? "w-full" : "flex-1"), style: {
52
+ transition: "background-color 0.15s, color 0.15s",
53
+ ...(isSelected
54
+ ? {
55
+ backgroundColor: colors.mode === "dark" ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.08)",
56
+ color: colors.text,
57
+ }
58
+ : {
59
+ color: colors.text,
60
+ backgroundColor: isHovered
61
+ ? (colors.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)")
62
+ : (colors.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)"),
63
+ }),
64
+ }, children: opt }, opt));
51
65
  }) })] }));
52
66
  }
@@ -0,0 +1,38 @@
1
+ /** Readable foreground color (black or white) for a given hex background. */
2
+ export declare function pickForegroundForBg(hex: string): string;
3
+ export type SnapColors = {
4
+ /** Resolved accent hex */
5
+ accent: string;
6
+ /** Readable foreground for accent bg (black or white) */
7
+ accentFg: string;
8
+ /** Primary button hover color */
9
+ accentHover: string;
10
+ /** Secondary/outline button hover color */
11
+ outlineHover: string;
12
+ /** Primary text color */
13
+ text: string;
14
+ /** Muted/secondary text color */
15
+ textMuted: string;
16
+ /** Border color */
17
+ border: string;
18
+ /** Muted background (tracks, containers) */
19
+ muted: string;
20
+ /** Surface/card background */
21
+ surface: string;
22
+ /** Input border */
23
+ inputBorder: string;
24
+ /** Input background */
25
+ inputBg: string;
26
+ /** Current color mode */
27
+ mode: "light" | "dark";
28
+ /** Resolve a palette color name to hex */
29
+ paletteHex: (name: string) => string;
30
+ /** Resolve a palette color name to hex, with accent fallback */
31
+ colorHex: (name: string | undefined) => string;
32
+ };
33
+ /**
34
+ * Returns fully resolved color values for snap components.
35
+ * All colors are concrete hex values (or color-mix expressions for hover states)
36
+ * so they can be used as inline styles, independent of host app CSS.
37
+ */
38
+ export declare function useSnapColors(): SnapColors;