@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
package/llms.txt CHANGED
@@ -30,7 +30,7 @@ Top-level fields: `version` (required, `"1.0"`), `theme` (optional, `{ accent: P
30
30
 
31
31
  `ui.root` is the ID of the root element. `ui.elements` is a flat map of element ID to element definition.
32
32
 
33
- ## Components (14 total)
33
+ ## Components (16 total)
34
34
 
35
35
  ### Display Components
36
36
 
@@ -75,6 +75,21 @@ Top-level fields: `version` (required, `"1.0"`), `theme` (optional, `{ accent: P
75
75
  - `weight` (optional): `"bold"` | `"normal"`. Default: `"normal"`
76
76
  - `align` (optional): `"left"` | `"center"` | `"right"`. Default: `"left"`
77
77
 
78
+ ### Data Components
79
+
80
+ **bar_chart** — Horizontal bar chart with labeled bars.
81
+ - `bars` (array, required, 1–6 items): each `{ label: string (max 40), value: number (≥0), color?: PaletteColor }`
82
+ - `max` (number, optional, ≥0): ceiling value; defaults to max bar value
83
+ - `color` (optional): PaletteColor. Default bar color. Default: `"accent"`
84
+
85
+ **cell_grid** — Colored cell grid, optionally interactive.
86
+ - `name` (string, optional): POST inputs key. Default: `"grid_tap"`
87
+ - `cols` (number, required, 2–32)
88
+ - `rows` (number, required, 2–16)
89
+ - `cells` (array, required): sparse list of `{ row, col, color?: PaletteColor, content?: string }`
90
+ - `gap` (optional): `"none"` (0px) | `"sm"` (1px) | `"md"` (2px) | `"lg"` (4px). Default: `"sm"`
91
+ - `select` (optional): `"off"` | `"single"` | `"multiple"`. Default: `"off"`. Taps write to `inputs[name]`
92
+
78
93
  ### Container Components
79
94
 
80
95
  **stack** — Layout container.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "1.10.0",
3
+ "version": "1.14.0",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
package/src/constants.ts CHANGED
@@ -3,3 +3,15 @@ export const SPEC_VERSION = "1.0" as const;
3
3
  export const MEDIA_TYPE = "application/vnd.farcaster.snap+json" as const;
4
4
 
5
5
  export const EFFECT_VALUES = ["confetti"] as const;
6
+
7
+ // ─── Pixel grid ────────────────────────────────────────
8
+ export const POST_GRID_TAP_KEY = "grid_tap" as const;
9
+ export const GRID_MIN_COLS = 2;
10
+ export const GRID_MAX_COLS = 32;
11
+ export const GRID_MIN_ROWS = 2;
12
+ export const GRID_MAX_ROWS = 16;
13
+ export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"] as const;
14
+
15
+ // ─── Bar chart ─────────────────────────────────────────
16
+ export const BAR_CHART_MAX_BARS = 6;
17
+ export const BAR_CHART_LABEL_MAX_CHARS = 40;
package/src/index.ts CHANGED
@@ -2,7 +2,12 @@ export type {
2
2
  Spec as SnapSpec,
3
3
  UIElement as SnapUIElement,
4
4
  } from "@json-render/core";
5
- export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES } from "./constants";
5
+ export {
6
+ SPEC_VERSION,
7
+ MEDIA_TYPE,
8
+ EFFECT_VALUES,
9
+ POST_GRID_TAP_KEY,
10
+ } from "./constants";
6
11
  export {
7
12
  DEFAULT_THEME_ACCENT,
8
13
  PALETTE_COLOR,
@@ -27,4 +32,3 @@ export {
27
32
  type SnapPayload,
28
33
  } from "./schemas";
29
34
  export { validateSnapResponse, type ValidationResult } from "./validator";
30
- export { type Middleware, useMiddleware } from "./middleware";
@@ -16,6 +16,8 @@ import { SnapStack } from "./components/stack";
16
16
  import { SnapSwitch } from "./components/switch";
17
17
  import { SnapText } from "./components/text";
18
18
  import { SnapToggleGroup } from "./components/toggle-group";
19
+ import { SnapBarChart } from "./components/bar-chart";
20
+ import { SnapCellGrid } from "./components/cell-grid";
19
21
 
20
22
  /**
21
23
  * Maps snap json-render catalog types to React components.
@@ -36,4 +38,6 @@ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
36
38
  switch: SnapSwitch,
37
39
  text: SnapText,
38
40
  toggle_group: SnapToggleGroup,
41
+ bar_chart: SnapBarChart,
42
+ cell_grid: SnapCellGrid,
39
43
  });
@@ -1,15 +1,11 @@
1
1
  "use client";
2
2
 
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";
6
+ import { useSnapColors } from "../hooks/use-snap-colors";
6
7
  import { ICON_MAP } from "./icon";
7
8
 
8
- const VARIANT_MAP: Record<string, "default" | "secondary"> = {
9
- primary: "default",
10
- secondary: "secondary",
11
- };
12
-
13
9
  export function SnapActionButton({
14
10
  element: { props },
15
11
  emit,
@@ -18,25 +14,36 @@ export function SnapActionButton({
18
14
  emit: (name: string) => void;
19
15
  }) {
20
16
  const label = String(props.label ?? "Action");
21
- const variant = VARIANT_MAP[String(props.variant ?? "secondary")] ?? "secondary";
17
+ const variant = String(props.variant ?? "secondary");
18
+ const isPrimary = variant === "primary";
22
19
  const iconName = props.icon ? String(props.icon) : undefined;
23
- const accentStyle = useSnapAccentScopeStyle();
20
+ const colors = useSnapColors();
21
+ const [hovered, setHovered] = useState(false);
24
22
 
25
23
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
26
24
 
25
+ const style = isPrimary
26
+ ? {
27
+ backgroundColor: hovered ? colors.accentHover : colors.accent,
28
+ color: colors.accentFg,
29
+ borderColor: "transparent",
30
+ }
31
+ : {
32
+ backgroundColor: hovered ? `color-mix(in srgb, ${colors.accent} 15%, transparent)` : colors.muted,
33
+ color: colors.text,
34
+ borderColor: "transparent",
35
+ };
36
+
27
37
  return (
28
- <div className="w-full min-w-0 flex-1" style={accentStyle}>
38
+ <div className="w-full min-w-0 flex-1">
29
39
  <Button
30
40
  type="button"
31
- variant={variant}
32
- className={cn(
33
- "w-full gap-2",
34
- variant === "default" &&
35
- "hover:!bg-[var(--snap-action-primary-hover)]",
36
- variant !== "default" &&
37
- "hover:!bg-[var(--snap-action-outline-hover)]",
38
- )}
41
+ variant={isPrimary ? "default" : "secondary"}
42
+ className={cn("w-full gap-2")}
43
+ style={style}
39
44
  onClick={() => emit("press")}
45
+ onPointerEnter={() => setHovered(true)}
46
+ onPointerLeave={() => setHovered(false)}
40
47
  >
41
48
  {Icon && <Icon size={16} />}
42
49
  {label}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { Badge } from "@neynar/ui/badge";
4
- import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
4
+ import { useSnapColors, pickForegroundForBg } from "../hooks/use-snap-colors";
5
5
  import { ICON_MAP } from "./icon";
6
6
 
7
7
  export function SnapBadge({
@@ -13,25 +13,22 @@ export function SnapBadge({
13
13
  const variant = String(props.variant ?? "default") as "default" | "outline";
14
14
  const color = props.color ? String(props.color) : undefined;
15
15
  const iconName = props.icon ? String(props.icon) : undefined;
16
- const accentStyle = useSnapAccentScopeStyle();
16
+ const colors = useSnapColors();
17
+
18
+ const badgeColor = colors.colorHex(color);
19
+ const badgeFg = variant === "default" ? pickForegroundForBg(badgeColor) : badgeColor;
17
20
 
18
- const isAccent = !color || color === "accent";
19
21
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
20
22
 
23
+ const style =
24
+ variant === "outline"
25
+ ? { borderColor: badgeColor, color: badgeColor, backgroundColor: "transparent" }
26
+ : { backgroundColor: badgeColor, color: badgeFg, borderColor: "transparent" };
27
+
21
28
  return (
22
- <span style={isAccent ? accentStyle : undefined}>
23
- <Badge
24
- variant={variant}
25
- className="gap-1"
26
- style={
27
- variant === "outline" && !isAccent
28
- ? { borderColor: `var(--snap-color-${color})`, color: `var(--snap-color-${color})` }
29
- : undefined
30
- }
31
- >
32
- {Icon && <Icon size={12} />}
33
- {content}
34
- </Badge>
35
- </span>
29
+ <Badge variant={variant} className="gap-1" style={style}>
30
+ {Icon && <Icon size={12} />}
31
+ {content}
32
+ </Badge>
36
33
  );
37
34
  }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { useSnapColors } from "../hooks/use-snap-colors";
4
+
5
+ export function SnapBarChart({
6
+ element: { props },
7
+ }: {
8
+ element: { props: Record<string, unknown> };
9
+ }) {
10
+ const colors = useSnapColors();
11
+ const bars = Array.isArray(props.bars) ? props.bars : [];
12
+ const chartColor = props.color ? String(props.color) : undefined;
13
+ const maxVal =
14
+ props.max != null
15
+ ? Number(props.max)
16
+ : Math.max(
17
+ ...bars.map((b: { value?: number }) => Number(b.value ?? 0)),
18
+ 1,
19
+ );
20
+
21
+ function barFill(bar: { color?: string }): string {
22
+ if (bar.color) return colors.colorHex(bar.color);
23
+ return colors.colorHex(chartColor);
24
+ }
25
+
26
+ return (
27
+ <div className="flex w-full flex-col gap-2">
28
+ {bars.map(
29
+ (
30
+ bar: { label?: string; value?: number; color?: string },
31
+ i: number,
32
+ ) => {
33
+ const value = Number(bar.value ?? 0);
34
+ const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
35
+ const fill = barFill(bar);
36
+ return (
37
+ <div key={i} className="flex w-full items-center gap-2">
38
+ <span
39
+ className="w-20 shrink-0 truncate text-right text-xs"
40
+ style={{ color: colors.textMuted }}
41
+ >
42
+ {String(bar.label ?? "")}
43
+ </span>
44
+ <div
45
+ className="h-2.5 flex-1 overflow-hidden rounded-full"
46
+ style={{ backgroundColor: colors.muted }}
47
+ >
48
+ <div
49
+ className="h-full rounded-full transition-all"
50
+ style={{
51
+ width: `${pct}%`,
52
+ minWidth: pct > 0 ? 4 : 0,
53
+ backgroundColor: fill,
54
+ }}
55
+ />
56
+ </div>
57
+ <span
58
+ className="w-8 shrink-0 text-xs tabular-nums"
59
+ style={{ color: colors.textMuted }}
60
+ >
61
+ {value}
62
+ </span>
63
+ </div>
64
+ );
65
+ },
66
+ )}
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useStateStore } from "@json-render/react";
5
+ import { cn } from "@neynar/ui/utils";
6
+ import { POST_GRID_TAP_KEY } from "@farcaster/snap";
7
+ import { useSnapColors } from "../hooks/use-snap-colors";
8
+
9
+ export function SnapCellGrid({
10
+ element: { props },
11
+ }: {
12
+ element: { props: Record<string, unknown> };
13
+ }) {
14
+ const { get, set } = useStateStore();
15
+ const colors = useSnapColors();
16
+ const cols = Number(props.cols ?? 2);
17
+ const rows = Number(props.rows ?? 2);
18
+ const select = String(props.select ?? "off");
19
+ const interactive = select !== "off";
20
+ const isMultiple = select === "multiple";
21
+ const cells = Array.isArray(props.cells) ? props.cells : [];
22
+ const gap = String(props.gap ?? "sm");
23
+ const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
24
+ const gapPx = gapMap[gap] ?? 1;
25
+
26
+ const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
27
+ const tapPath = `/inputs/${name}`;
28
+ const tapRaw = get(tapPath);
29
+
30
+ // Parse selection — single mode: "row,col" string; multi mode: "row,col|row,col|..." string
31
+ const selectedSet = new Set<string>();
32
+ if (typeof tapRaw === "string" && tapRaw.length > 0) {
33
+ for (const part of tapRaw.split("|")) {
34
+ if (part.includes(",")) selectedSet.add(part);
35
+ }
36
+ }
37
+
38
+ const isSelected = (r: number, c: number) => selectedSet.has(`${r},${c}`);
39
+
40
+ const handleTap = (r: number, c: number) => {
41
+ const key = `${r},${c}`;
42
+ if (isMultiple) {
43
+ const next = new Set(selectedSet);
44
+ if (next.has(key)) next.delete(key);
45
+ else next.add(key);
46
+ set(tapPath, [...next].join("|"));
47
+ } else {
48
+ set(tapPath, key);
49
+ }
50
+ };
51
+
52
+ const cellMap = new Map<string, { color?: string; content?: string }>();
53
+ for (const c of cells) {
54
+ cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
55
+ color: c.color as string | undefined,
56
+ content: c.content != null ? String(c.content) : undefined,
57
+ });
58
+ }
59
+
60
+ const cellEls: ReactNode[] = [];
61
+ for (let r = 0; r < rows; r++) {
62
+ for (let c = 0; c < cols; c++) {
63
+ const cell = cellMap.get(`${r},${c}`);
64
+ const selected = interactive && isSelected(r, c);
65
+ const bg = cell?.color ? colors.colorHex(cell.color) : "transparent";
66
+
67
+ cellEls.push(
68
+ <div
69
+ key={`${r}-${c}`}
70
+ role={interactive ? "button" : undefined}
71
+ tabIndex={interactive ? 0 : undefined}
72
+ onClick={interactive ? () => handleTap(r, c) : undefined}
73
+ onKeyDown={
74
+ interactive
75
+ ? (e) => {
76
+ if (e.key === "Enter" || e.key === " ") {
77
+ e.preventDefault();
78
+ handleTap(r, c);
79
+ }
80
+ }
81
+ : undefined
82
+ }
83
+ className={cn(
84
+ "flex min-h-7 items-center justify-center rounded text-xs font-semibold",
85
+ interactive ? "cursor-pointer select-none" : "cursor-default",
86
+ )}
87
+ style={{
88
+ background: bg,
89
+ // Two-layer ring: 1px white/black inner + 2px accent outer
90
+ boxShadow: selected
91
+ ? `inset 0 0 0 1px ${colors.mode === "dark" ? "#000" : "#fff"}, inset 0 0 0 2px ${colors.mode === "dark" ? "#fff" : "#000"}`
92
+ : undefined,
93
+ }}
94
+ >
95
+ {cell?.content ?? ""}
96
+ </div>,
97
+ );
98
+ }
99
+ }
100
+
101
+ const selectionLabel = interactive && selectedSet.size > 0
102
+ ? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
103
+ : null;
104
+
105
+ return (
106
+ <div>
107
+ <div
108
+ className="grid w-full rounded-lg p-1"
109
+ style={{
110
+ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
111
+ gap: gapPx,
112
+ backgroundColor: colors.muted,
113
+ }}
114
+ >
115
+ {cellEls}
116
+ </div>
117
+ {selectionLabel && (
118
+ <div className="mt-1.5 truncate text-xs font-mono" style={{ color: colors.textMuted }}>
119
+ {selectionLabel}
120
+ </div>
121
+ )}
122
+ </div>
123
+ );
124
+ }
@@ -36,7 +36,7 @@ import {
36
36
  TrendingDown,
37
37
  type LucideIcon,
38
38
  } from "lucide-react";
39
- import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
39
+ import { useSnapColors } from "../hooks/use-snap-colors";
40
40
 
41
41
  export const ICON_MAP: Record<string, LucideIcon> = {
42
42
  "arrow-right": ArrowRight,
@@ -87,29 +87,16 @@ export function SnapIcon({
87
87
  const name = String(props.name ?? "info");
88
88
  const size = SIZE_PX[String(props.size ?? "md")] ?? 20;
89
89
  const color = props.color ? String(props.color) : undefined;
90
- const accentStyle = useSnapAccentScopeStyle();
90
+ const colors = useSnapColors();
91
91
 
92
92
  const Icon = ICON_MAP[name];
93
93
  if (!Icon) return null;
94
94
 
95
- const isAccent = !color || color === "accent";
95
+ const iconColor = colors.colorHex(color);
96
96
 
97
97
  return (
98
- <span
99
- style={{
100
- display: "inline-flex",
101
- alignItems: "center",
102
- ...(isAccent ? accentStyle : {}),
103
- }}
104
- >
105
- <Icon
106
- size={size}
107
- style={
108
- isAccent
109
- ? { color: "var(--snap-accent, currentColor)" }
110
- : { color: `var(--snap-color-${color}, currentColor)` }
111
- }
112
- />
98
+ <span style={{ display: "inline-flex", alignItems: "center" }}>
99
+ <Icon size={size} style={{ color: iconColor }} />
113
100
  </span>
114
101
  );
115
102
  }
@@ -4,32 +4,43 @@ 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";
7
8
 
8
9
  export function SnapInput({
9
10
  element: { props },
10
11
  }: {
11
12
  element: { props: Record<string, unknown> };
12
13
  }) {
13
- const id = useId();
14
14
  const { get, set } = useStateStore();
15
+ const colors = useSnapColors();
15
16
  const name = String(props.name ?? "input");
16
- const path = `/inputs/${name}`;
17
+ const type = String(props.type ?? "text");
17
18
  const label = props.label ? String(props.label) : undefined;
18
19
  const placeholder = props.placeholder ? String(props.placeholder) : undefined;
19
- const maxLength =
20
- typeof props.maxLength === "number" ? props.maxLength : undefined;
21
- const raw = get(path);
22
- const value = typeof raw === "string" ? raw : "";
20
+ const maxLength = props.maxLength ? Number(props.maxLength) : undefined;
21
+ const path = `/inputs/${name}`;
22
+ const value = (get(path) as string) ?? (props.defaultValue != null ? String(props.defaultValue) : "");
23
+ const id = useId();
23
24
 
24
25
  return (
25
26
  <div className="w-full space-y-1.5">
26
- {label && <Label htmlFor={id}>{label}</Label>}
27
+ {label && (
28
+ <Label htmlFor={id} style={{ color: colors.text }}>
29
+ {label}
30
+ </Label>
31
+ )}
27
32
  <Input
28
33
  id={id}
29
- value={value}
30
- onChange={(e) => set(path, e.target.value)}
34
+ type={type === "number" ? "number" : "text"}
31
35
  placeholder={placeholder}
32
36
  maxLength={maxLength}
37
+ value={value}
38
+ onChange={(e) => set(path, e.target.value)}
39
+ style={{
40
+ backgroundColor: colors.inputBg,
41
+ borderColor: colors.inputBorder,
42
+ color: colors.text,
43
+ }}
33
44
  />
34
45
  </div>
35
46
  );
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Children, type ReactNode, Fragment } from "react";
4
4
  import { cn } from "@neynar/ui/utils";
5
+ import { useSnapColors } from "../hooks/use-snap-colors";
5
6
 
6
7
  const GAP_MAP: Record<string, string> = {
7
8
  none: "gap-0",
@@ -21,6 +22,7 @@ export function SnapItemGroup({
21
22
  const separator = Boolean(props.separator);
22
23
  const gap = GAP_MAP[String(props.gap ?? "sm")] ?? "gap-1";
23
24
  const items = Children.toArray(children);
25
+ const colors = useSnapColors();
24
26
 
25
27
  return (
26
28
  <div
@@ -29,11 +31,12 @@ export function SnapItemGroup({
29
31
  border && "rounded-lg border",
30
32
  gap,
31
33
  )}
34
+ style={border ? { borderColor: colors.border } : undefined}
32
35
  >
33
36
  {items.map((child, i) => (
34
37
  <Fragment key={i}>
35
38
  {separator && i > 0 && (
36
- <div className="h-px bg-border" />
39
+ <div className="h-px" style={{ backgroundColor: colors.border }} />
37
40
  )}
38
41
  {child}
39
42
  </Fragment>
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import type { ReactNode } from "react";
4
3
  import {
5
4
  Item,
6
5
  ItemContent,
@@ -8,26 +7,30 @@ import {
8
7
  ItemDescription,
9
8
  ItemActions,
10
9
  } from "@neynar/ui/item";
10
+ import { useSnapColors } from "../hooks/use-snap-colors";
11
11
 
12
12
  export function SnapItem({
13
- element: { props },
13
+ element: { props, children: childIds },
14
14
  children,
15
15
  }: {
16
- element: { props: Record<string, unknown> };
17
- children?: ReactNode;
16
+ element: { props: Record<string, unknown>; children?: string[] };
17
+ children?: React.ReactNode;
18
18
  }) {
19
19
  const title = String(props.title ?? "");
20
20
  const description = props.description ? String(props.description) : undefined;
21
- const variant =
22
- (props.variant as "default") ?? "default";
21
+ const colors = useSnapColors();
23
22
 
24
23
  return (
25
- <Item variant={variant} className="flex-1 py-1.5 px-2.5">
24
+ <Item className="flex-1 py-1.5 px-2.5">
26
25
  <ItemContent className="gap-0.5">
27
- <ItemTitle>{title}</ItemTitle>
28
- {description && <ItemDescription className="mt-0">{description}</ItemDescription>}
26
+ <ItemTitle style={{ color: colors.text }}>{title}</ItemTitle>
27
+ {description && (
28
+ <ItemDescription className="mt-0" style={{ color: colors.textMuted }}>
29
+ {description}
30
+ </ItemDescription>
31
+ )}
29
32
  </ItemContent>
30
- {children && <ItemActions>{children}</ItemActions>}
33
+ {childIds && childIds.length > 0 && <ItemActions>{children}</ItemActions>}
31
34
  </Item>
32
35
  );
33
36
  }
@@ -1,27 +1,32 @@
1
1
  "use client";
2
2
 
3
- import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
3
+ import { useSnapColors } from "../hooks/use-snap-colors";
4
4
 
5
5
  export function SnapProgress({
6
6
  element: { props },
7
7
  }: {
8
8
  element: { props: Record<string, unknown> };
9
9
  }) {
10
- const accentStyle = useSnapAccentScopeStyle();
10
+ const colors = useSnapColors();
11
11
  const value = Number(props.value ?? 0);
12
12
  const max = Math.max(1, Number(props.max ?? 100));
13
13
  const percent = Math.min(100, Math.max(0, (value / max) * 100));
14
14
  const label = props.label ? String(props.label) : null;
15
15
 
16
16
  return (
17
- <div className="flex w-full flex-1 flex-col gap-1" style={accentStyle}>
17
+ <div className="flex w-full flex-1 flex-col gap-1">
18
18
  {label && (
19
- <span className="text-muted-foreground text-xs">{label}</span>
19
+ <span className="text-xs" style={{ color: colors.textMuted }}>
20
+ {label}
21
+ </span>
20
22
  )}
21
- <div className="bg-muted h-2.5 w-full overflow-hidden rounded-full">
23
+ <div
24
+ className="h-2.5 w-full overflow-hidden rounded-full"
25
+ style={{ backgroundColor: colors.muted }}
26
+ >
22
27
  <div
23
- className="h-full rounded-full bg-primary transition-all"
24
- style={{ width: `${percent}%` }}
28
+ className="h-full rounded-full transition-all"
29
+ style={{ width: `${percent}%`, backgroundColor: colors.accent }}
25
30
  />
26
31
  </div>
27
32
  </div>
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { Separator } from "@neynar/ui/separator";
4
+ import { useSnapColors } from "../hooks/use-snap-colors";
4
5
 
5
6
  export function SnapSeparator({
6
7
  element: { props },
@@ -9,6 +10,12 @@ export function SnapSeparator({
9
10
  }) {
10
11
  const orientation =
11
12
  (props.orientation as "horizontal" | "vertical") ?? "horizontal";
13
+ const colors = useSnapColors();
12
14
 
13
- return <Separator orientation={orientation} />;
15
+ return (
16
+ <Separator
17
+ orientation={orientation}
18
+ style={{ backgroundColor: colors.border }}
19
+ />
20
+ );
14
21
  }