@farcaster/snap 1.5.2 → 1.7.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 (208) hide show
  1. package/dist/constants.d.ts +0 -107
  2. package/dist/constants.js +0 -148
  3. package/dist/dataStore.d.ts +12 -0
  4. package/dist/dataStore.js +35 -0
  5. package/dist/index.d.ts +5 -3
  6. package/dist/index.js +4 -3
  7. package/dist/react/accent-context.d.ts +6 -0
  8. package/dist/react/accent-context.js +10 -0
  9. package/dist/react/catalog-renderer.d.ts +5 -0
  10. package/dist/react/catalog-renderer.js +37 -0
  11. package/dist/react/components/action-button.d.ts +6 -0
  12. package/dist/react/components/action-button.js +22 -0
  13. package/dist/react/components/badge.d.ts +5 -0
  14. package/dist/react/components/badge.js +18 -0
  15. package/dist/react/components/icon.d.ts +7 -0
  16. package/dist/react/components/icon.js +60 -0
  17. package/dist/react/components/image.d.ts +5 -0
  18. package/dist/react/components/image.js +15 -0
  19. package/dist/react/components/input.d.ts +5 -0
  20. package/dist/react/components/input.js +18 -0
  21. package/dist/react/components/item-group.d.ts +7 -0
  22. package/dist/react/components/item-group.js +17 -0
  23. package/dist/react/components/item.d.ts +7 -0
  24. package/dist/react/components/item.js +9 -0
  25. package/dist/react/components/progress.d.ts +5 -0
  26. package/dist/react/components/progress.js +11 -0
  27. package/dist/react/components/separator.d.ts +5 -0
  28. package/dist/react/components/separator.js +7 -0
  29. package/dist/react/components/slider.d.ts +5 -0
  30. package/dist/react/components/slider.js +21 -0
  31. package/dist/react/components/stack.d.ts +7 -0
  32. package/dist/react/components/stack.js +32 -0
  33. package/dist/react/components/switch.d.ts +5 -0
  34. package/dist/react/components/switch.js +23 -0
  35. package/dist/react/components/text.d.ts +5 -0
  36. package/dist/react/components/text.js +25 -0
  37. package/dist/react/components/toggle-group.d.ts +5 -0
  38. package/dist/react/components/toggle-group.js +52 -0
  39. package/dist/react/hooks/use-snap-accent.d.ts +13 -0
  40. package/dist/react/hooks/use-snap-accent.js +32 -0
  41. package/dist/react/index.d.ts +47 -0
  42. package/dist/react/index.js +191 -0
  43. package/dist/react/lib/preview-primary-css.d.ts +6 -0
  44. package/dist/react/lib/preview-primary-css.js +43 -0
  45. package/dist/react/lib/resolve-palette-hex.d.ts +2 -0
  46. package/dist/react/lib/resolve-palette-hex.js +10 -0
  47. package/dist/react-native/catalog-renderer.d.ts +5 -0
  48. package/dist/react-native/catalog-renderer.js +36 -0
  49. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  50. package/dist/react-native/components/snap-action-button.js +68 -0
  51. package/dist/react-native/components/snap-badge.d.ts +2 -0
  52. package/dist/react-native/components/snap-badge.js +38 -0
  53. package/dist/react-native/components/snap-icon.d.ts +5 -0
  54. package/dist/react-native/components/snap-icon.js +56 -0
  55. package/dist/react-native/components/snap-image.d.ts +2 -0
  56. package/dist/react-native/components/snap-image.js +24 -0
  57. package/dist/react-native/components/snap-input.d.ts +2 -0
  58. package/dist/react-native/components/snap-input.js +36 -0
  59. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  60. package/dist/react-native/components/snap-item-group.js +23 -0
  61. package/dist/react-native/components/snap-item.d.ts +5 -0
  62. package/dist/react-native/components/snap-item.js +45 -0
  63. package/dist/react-native/components/snap-progress.d.ts +2 -0
  64. package/dist/react-native/components/snap-progress.js +26 -0
  65. package/dist/react-native/components/snap-separator.d.ts +2 -0
  66. package/dist/react-native/components/snap-separator.js +23 -0
  67. package/dist/react-native/components/snap-slider.d.ts +2 -0
  68. package/dist/react-native/components/snap-slider.js +42 -0
  69. package/dist/react-native/components/snap-stack.d.ts +5 -0
  70. package/dist/react-native/components/snap-stack.js +49 -0
  71. package/dist/react-native/components/snap-switch.d.ts +2 -0
  72. package/dist/react-native/components/snap-switch.js +30 -0
  73. package/dist/react-native/components/snap-text.d.ts +2 -0
  74. package/dist/react-native/components/snap-text.js +37 -0
  75. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  76. package/dist/react-native/components/snap-toggle-group.js +100 -0
  77. package/dist/react-native/index.d.ts +52 -0
  78. package/dist/react-native/index.js +155 -0
  79. package/dist/react-native/theme.d.ts +21 -0
  80. package/dist/react-native/theme.js +37 -0
  81. package/dist/react-native/use-snap-palette.d.ts +13 -0
  82. package/dist/react-native/use-snap-palette.js +48 -0
  83. package/dist/schemas.d.ts +14 -1629
  84. package/dist/schemas.js +14 -526
  85. package/dist/ui/badge.d.ts +52 -0
  86. package/dist/ui/badge.js +9 -0
  87. package/dist/ui/button.d.ts +42 -28
  88. package/dist/ui/button.js +7 -9
  89. package/dist/ui/catalog.d.ts +281 -156
  90. package/dist/ui/catalog.js +102 -83
  91. package/dist/ui/icon.d.ts +56 -0
  92. package/dist/ui/icon.js +51 -0
  93. package/dist/ui/image.d.ts +1 -0
  94. package/dist/ui/image.js +2 -2
  95. package/dist/ui/index.d.ts +20 -22
  96. package/dist/ui/index.js +10 -11
  97. package/dist/ui/input.d.ts +17 -0
  98. package/dist/ui/input.js +13 -0
  99. package/dist/ui/item-group.d.ts +12 -0
  100. package/dist/ui/item-group.js +7 -0
  101. package/dist/ui/item.d.ts +14 -0
  102. package/dist/ui/item.js +9 -0
  103. package/dist/ui/progress.d.ts +1 -11
  104. package/dist/ui/progress.js +21 -4
  105. package/dist/ui/schema.d.ts +1 -1
  106. package/dist/ui/schema.js +3 -3
  107. package/dist/ui/separator.d.ts +9 -0
  108. package/dist/ui/separator.js +5 -0
  109. package/dist/ui/slider.d.ts +4 -3
  110. package/dist/ui/slider.js +34 -5
  111. package/dist/ui/stack.d.ts +22 -1
  112. package/dist/ui/stack.js +8 -1
  113. package/dist/ui/switch.d.ts +8 -0
  114. package/dist/ui/switch.js +7 -0
  115. package/dist/ui/text.d.ts +15 -7
  116. package/dist/ui/text.js +8 -4
  117. package/dist/ui/toggle-group.d.ts +23 -0
  118. package/dist/ui/toggle-group.js +19 -0
  119. package/dist/validator.d.ts +5 -1
  120. package/dist/validator.js +6 -136
  121. package/package.json +78 -53
  122. package/src/constants.ts +0 -179
  123. package/src/dataStore.ts +62 -0
  124. package/src/index.ts +10 -20
  125. package/src/react/accent-context.tsx +29 -0
  126. package/src/react/catalog-renderer.tsx +39 -0
  127. package/src/react/components/action-button.tsx +48 -0
  128. package/src/react/components/badge.tsx +37 -0
  129. package/src/react/components/icon.tsx +115 -0
  130. package/src/react/components/image.tsx +33 -0
  131. package/src/react/components/input.tsx +36 -0
  132. package/src/react/components/item-group.tsx +43 -0
  133. package/src/react/components/item.tsx +33 -0
  134. package/src/react/components/progress.tsx +29 -0
  135. package/src/react/components/separator.tsx +14 -0
  136. package/src/react/components/slider.tsx +43 -0
  137. package/src/react/components/stack.tsx +55 -0
  138. package/src/react/components/switch.tsx +46 -0
  139. package/src/react/components/text.tsx +43 -0
  140. package/src/react/components/toggle-group.tsx +85 -0
  141. package/src/react/hooks/use-snap-accent.ts +45 -0
  142. package/src/react/index.tsx +321 -0
  143. package/src/react/lib/preview-primary-css.ts +57 -0
  144. package/src/react/lib/resolve-palette-hex.ts +20 -0
  145. package/src/react-native/catalog-renderer.tsx +37 -0
  146. package/src/react-native/components/snap-action-button.tsx +92 -0
  147. package/src/react-native/components/snap-badge.tsx +57 -0
  148. package/src/react-native/components/snap-icon.tsx +102 -0
  149. package/src/react-native/components/snap-image.tsx +38 -0
  150. package/src/react-native/components/snap-input.tsx +57 -0
  151. package/src/react-native/components/snap-item-group.tsx +43 -0
  152. package/src/react-native/components/snap-item.tsx +70 -0
  153. package/src/react-native/components/snap-progress.tsx +40 -0
  154. package/src/react-native/components/snap-separator.tsx +32 -0
  155. package/src/react-native/components/snap-slider.tsx +82 -0
  156. package/src/react-native/components/snap-stack.tsx +66 -0
  157. package/src/react-native/components/snap-switch.tsx +45 -0
  158. package/src/react-native/components/snap-text.tsx +53 -0
  159. package/src/react-native/components/snap-toggle-group.tsx +128 -0
  160. package/src/react-native/index.tsx +267 -0
  161. package/src/react-native/theme.tsx +73 -0
  162. package/src/react-native/use-snap-palette.ts +64 -0
  163. package/src/schemas.ts +18 -644
  164. package/src/ui/badge.ts +13 -0
  165. package/src/ui/button.ts +9 -12
  166. package/src/ui/catalog.ts +106 -86
  167. package/src/ui/icon.ts +56 -0
  168. package/src/ui/image.ts +3 -2
  169. package/src/ui/index.ts +26 -29
  170. package/src/ui/input.ts +17 -0
  171. package/src/ui/item-group.ts +11 -0
  172. package/src/ui/item.ts +13 -0
  173. package/src/ui/progress.ts +25 -7
  174. package/src/ui/schema.ts +3 -3
  175. package/src/ui/separator.ts +9 -0
  176. package/src/ui/slider.ts +40 -10
  177. package/src/ui/stack.ts +9 -1
  178. package/src/ui/switch.ts +11 -0
  179. package/src/ui/text.ts +9 -4
  180. package/src/ui/toggle-group.ts +23 -0
  181. package/src/validator.ts +6 -176
  182. package/dist/ui/bar-chart.d.ts +0 -30
  183. package/dist/ui/bar-chart.js +0 -15
  184. package/dist/ui/button-group.d.ts +0 -19
  185. package/dist/ui/button-group.js +0 -18
  186. package/dist/ui/divider.d.ts +0 -3
  187. package/dist/ui/divider.js +0 -2
  188. package/dist/ui/grid.d.ts +0 -22
  189. package/dist/ui/grid.js +0 -16
  190. package/dist/ui/group.d.ts +0 -7
  191. package/dist/ui/group.js +0 -5
  192. package/dist/ui/list.d.ts +0 -13
  193. package/dist/ui/list.js +0 -13
  194. package/dist/ui/spacer.d.ts +0 -9
  195. package/dist/ui/spacer.js +0 -5
  196. package/dist/ui/text-input.d.ts +0 -7
  197. package/dist/ui/text-input.js +0 -12
  198. package/dist/ui/toggle.d.ts +0 -7
  199. package/dist/ui/toggle.js +0 -6
  200. package/src/ui/bar-chart.ts +0 -20
  201. package/src/ui/button-group.ts +0 -26
  202. package/src/ui/divider.ts +0 -5
  203. package/src/ui/grid.ts +0 -25
  204. package/src/ui/group.ts +0 -8
  205. package/src/ui/list.ts +0 -17
  206. package/src/ui/spacer.ts +0 -8
  207. package/src/ui/text-input.ts +0 -15
  208. package/src/ui/toggle.ts +0 -9
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import {
5
+ Item,
6
+ ItemContent,
7
+ ItemTitle,
8
+ ItemDescription,
9
+ ItemActions,
10
+ } from "@neynar/ui/item";
11
+
12
+ export function SnapItem({
13
+ element: { props },
14
+ children,
15
+ }: {
16
+ element: { props: Record<string, unknown> };
17
+ children?: ReactNode;
18
+ }) {
19
+ const title = String(props.title ?? "");
20
+ const description = props.description ? String(props.description) : undefined;
21
+ const variant =
22
+ (props.variant as "default" | "outline" | "muted") ?? "default";
23
+
24
+ return (
25
+ <Item variant={variant} className={`flex-1 py-1.5 px-2.5 ${variant === "muted" ? "!bg-border/20" : ""}`}>
26
+ <ItemContent className="gap-0.5">
27
+ <ItemTitle>{title}</ItemTitle>
28
+ {description && <ItemDescription className="mt-0">{description}</ItemDescription>}
29
+ </ItemContent>
30
+ {children && <ItemActions>{children}</ItemActions>}
31
+ </Item>
32
+ );
33
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
4
+
5
+ export function SnapProgress({
6
+ element: { props },
7
+ }: {
8
+ element: { props: Record<string, unknown> };
9
+ }) {
10
+ const accentStyle = useSnapAccentScopeStyle();
11
+ const value = Number(props.value ?? 0);
12
+ const max = Math.max(1, Number(props.max ?? 100));
13
+ const percent = Math.min(100, Math.max(0, (value / max) * 100));
14
+ const label = props.label ? String(props.label) : null;
15
+
16
+ return (
17
+ <div className="flex w-full flex-1 flex-col gap-1" style={accentStyle}>
18
+ {label && (
19
+ <span className="text-muted-foreground text-xs">{label}</span>
20
+ )}
21
+ <div className="bg-muted h-2.5 w-full overflow-hidden rounded-full">
22
+ <div
23
+ className="h-full rounded-full bg-primary transition-all"
24
+ style={{ width: `${percent}%` }}
25
+ />
26
+ </div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import { Separator } from "@neynar/ui/separator";
4
+
5
+ export function SnapSeparator({
6
+ element: { props },
7
+ }: {
8
+ element: { props: Record<string, unknown> };
9
+ }) {
10
+ const orientation =
11
+ (props.orientation as "horizontal" | "vertical") ?? "horizontal";
12
+
13
+ return <Separator orientation={orientation} />;
14
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { useStateStore } from "@json-render/react";
4
+ import { Label } from "@neynar/ui/label";
5
+ import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
6
+
7
+ // TODO: switch back to @neynar/ui/slider once Base UI fixes the inline
8
+ // <script> tag that triggers a React console warning on client render.
9
+
10
+ export function SnapSlider({
11
+ element: { props },
12
+ }: {
13
+ element: { props: Record<string, unknown> };
14
+ }) {
15
+ const { get, set } = useStateStore();
16
+ const accentStyle = useSnapAccentScopeStyle();
17
+
18
+ const name = String(props.name ?? "slider");
19
+ const path = `/inputs/${name}`;
20
+ const label = props.label ? String(props.label) : undefined;
21
+ const min = Number(props.min ?? 0);
22
+ const max = Number(props.max ?? 100);
23
+ const step = props.step != null ? Number(props.step) : 1;
24
+ const fallback = props.defaultValue != null ? Number(props.defaultValue) : (min + max) / 2;
25
+ const raw = get(path);
26
+ const value = raw === undefined || raw === null ? fallback : Number(raw);
27
+
28
+ return (
29
+ <div className="flex w-full flex-col gap-1.5" style={accentStyle}>
30
+ {label && <Label>{label}</Label>}
31
+ <input
32
+ type="range"
33
+ min={min}
34
+ max={max}
35
+ step={step}
36
+ value={value}
37
+ onChange={(e) => set(path, Number(e.target.value))}
38
+ className="w-full h-2.5 rounded-full appearance-none bg-muted cursor-pointer"
39
+ style={{ accentColor: "var(--primary)" }}
40
+ />
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { cn } from "@neynar/ui/utils";
5
+
6
+ const VGAP: Record<string, string> = {
7
+ none: "gap-0",
8
+ sm: "gap-2",
9
+ md: "gap-4",
10
+ lg: "gap-6",
11
+ };
12
+
13
+ const HGAP: Record<string, string> = {
14
+ none: "gap-0",
15
+ sm: "gap-1",
16
+ md: "gap-2",
17
+ lg: "gap-3",
18
+ };
19
+
20
+ const JUSTIFY: Record<string, string> = {
21
+ start: "justify-start",
22
+ center: "justify-center",
23
+ end: "justify-end",
24
+ between: "justify-between",
25
+ around: "justify-around",
26
+ };
27
+
28
+ export function SnapStack({
29
+ element: { props },
30
+ children,
31
+ }: {
32
+ element: { props: Record<string, unknown> };
33
+ children?: ReactNode;
34
+ }) {
35
+ const direction = String(props.direction ?? "vertical");
36
+ const gapKey = String(props.gap ?? "md");
37
+ const isHorizontal = direction === "horizontal";
38
+ const gap = isHorizontal
39
+ ? (HGAP[gapKey] ?? "gap-2")
40
+ : (VGAP[gapKey] ?? "gap-4");
41
+ const justify = props.justify ? JUSTIFY[String(props.justify)] : undefined;
42
+
43
+ return (
44
+ <div
45
+ className={cn(
46
+ "flex w-full",
47
+ isHorizontal ? "flex-row items-center flex-wrap" : "flex-col",
48
+ gap,
49
+ justify,
50
+ )}
51
+ >
52
+ {children}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { useId } from "react";
4
+ import { useStateStore } from "@json-render/react";
5
+ import { Label } from "@neynar/ui/label";
6
+ import { Switch } from "@neynar/ui/switch";
7
+ import { useColorMode } from "@neynar/ui/color-mode";
8
+ import { cn } from "@neynar/ui/utils";
9
+ import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
10
+
11
+ export function SnapSwitch({
12
+ element: { props },
13
+ }: {
14
+ element: { props: Record<string, unknown> };
15
+ }) {
16
+ const id = useId();
17
+ const { get, set } = useStateStore();
18
+ const { mode } = useColorMode();
19
+ const accentStyle = useSnapAccentScopeStyle();
20
+ const name = String(props.name ?? "switch");
21
+ const path = `/inputs/${name}`;
22
+ const label = props.label ? String(props.label) : undefined;
23
+ const fallback = Boolean(props.defaultChecked ?? false);
24
+ const raw = get(path);
25
+ const checked = raw === undefined || raw === null ? fallback : Boolean(raw);
26
+
27
+ return (
28
+ <div className="flex items-center justify-between gap-3">
29
+ {label && (
30
+ <Label htmlFor={id} className="text-foreground font-normal">
31
+ {label}
32
+ </Label>
33
+ )}
34
+ <Switch
35
+ id={id}
36
+ checked={checked}
37
+ onCheckedChange={(v) => set(path, v)}
38
+ style={accentStyle}
39
+ className={cn(
40
+ mode === "light" &&
41
+ "data-unchecked:!bg-border data-unchecked:!border-(--input-border)",
42
+ )}
43
+ />
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { Text, Title } from "@neynar/ui/typography";
4
+
5
+ const SIZE_MAP = {
6
+ lg: { component: "title", textSize: undefined, order: 3 },
7
+ md: { component: "text", textSize: "base" as const, order: undefined },
8
+ sm: { component: "text", textSize: "sm" as const, order: undefined },
9
+ } as const;
10
+
11
+ const WEIGHT_MAP = {
12
+ bold: "bold",
13
+ medium: "medium",
14
+ normal: "normal",
15
+ } as const;
16
+
17
+ export function SnapText({
18
+ element: { props },
19
+ }: {
20
+ element: { props: Record<string, unknown> };
21
+ }) {
22
+ const content = String(props.content ?? "");
23
+ const size = String(props.size ?? "md") as "lg" | "md" | "sm";
24
+ const weight = props.weight ? String(props.weight) as "bold" | "medium" | "normal" : undefined;
25
+ const align = (props.align as "left" | "center" | "right") ?? undefined;
26
+ const config = SIZE_MAP[size] ?? SIZE_MAP.md;
27
+
28
+ const alignClass = align === "center" ? "text-center" : align === "right" ? "text-right" : "";
29
+
30
+ if (config.component === "title") {
31
+ return (
32
+ <Title order={config.order} weight={weight ?? "bold"} className={`flex-1 ${alignClass}`}>
33
+ {content}
34
+ </Title>
35
+ );
36
+ }
37
+
38
+ return (
39
+ <Text size={config.textSize} weight={weight} align={align} className="flex-1">
40
+ {content}
41
+ </Text>
42
+ );
43
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { useStateStore } from "@json-render/react";
4
+ import { Label } from "@neynar/ui/label";
5
+ import { cn } from "@neynar/ui/utils";
6
+ import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
7
+
8
+ export function SnapToggleGroup({
9
+ element: { props },
10
+ }: {
11
+ element: { props: Record<string, unknown> };
12
+ }) {
13
+ const { get, set } = useStateStore();
14
+ const accentStyle = useSnapAccentScopeStyle();
15
+ const name = String(props.name ?? "toggle_group");
16
+ const path = `/inputs/${name}`;
17
+ const label = props.label ? String(props.label) : undefined;
18
+ const isMultiple = Boolean(props.multiple);
19
+ const orientation = String(props.orientation ?? "horizontal");
20
+ const options = Array.isArray(props.options)
21
+ ? (props.options as string[])
22
+ : [];
23
+
24
+ const raw = get(path);
25
+ const defaultValue = props.defaultValue;
26
+
27
+ const selected = (() => {
28
+ if (raw !== undefined && raw !== null) {
29
+ return isMultiple
30
+ ? Array.isArray(raw) ? (raw as string[]) : []
31
+ : typeof raw === "string" ? [raw] : [];
32
+ }
33
+ if (defaultValue !== undefined) {
34
+ return Array.isArray(defaultValue) ? defaultValue as string[] : [String(defaultValue)];
35
+ }
36
+ return [];
37
+ })();
38
+
39
+ const toggle = (opt: string) => {
40
+ if (isMultiple) {
41
+ const current = Array.isArray(raw) ? (raw as string[]) : [];
42
+ if (current.includes(opt)) {
43
+ set(path, current.filter((v) => v !== opt));
44
+ } else {
45
+ set(path, [...current, opt]);
46
+ }
47
+ } else {
48
+ set(path, opt);
49
+ }
50
+ };
51
+
52
+ const isVertical = orientation === "vertical";
53
+
54
+ return (
55
+ <div className="w-full space-y-1.5" style={accentStyle}>
56
+ {label && <Label>{label}</Label>}
57
+ <div
58
+ className={cn(
59
+ "flex gap-1 rounded-lg bg-border/20 p-1",
60
+ isVertical ? "flex-col" : "flex-row",
61
+ )}
62
+ >
63
+ {options.map((opt) => {
64
+ const isSelected = selected.includes(opt);
65
+ return (
66
+ <button
67
+ key={opt}
68
+ type="button"
69
+ onClick={() => toggle(opt)}
70
+ className={cn(
71
+ "rounded-md px-3 py-2 text-sm font-medium transition-colors",
72
+ isVertical ? "w-full" : "flex-1",
73
+ isSelected
74
+ ? "bg-primary text-primary-foreground"
75
+ : "text-foreground hover:bg-border/30",
76
+ )}
77
+ >
78
+ {opt}
79
+ </button>
80
+ );
81
+ })}
82
+ </div>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { useMemo, type CSSProperties } from "react";
4
+ import { useStateStore } from "@json-render/react";
5
+ import type { PaletteColor } from "@farcaster/snap";
6
+ import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX } from "@farcaster/snap";
7
+ import { useColorMode } from "@neynar/ui/color-mode";
8
+ import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
9
+ import { snapPreviewPrimaryCssProperties } from "../lib/preview-primary-css";
10
+ import { useSnapPreviewPageAccent } from "../accent-context";
11
+
12
+ /**
13
+ * CSS variables so Neynar controls (`bg-primary`, `data-checked:bg-primary`, etc.)
14
+ * use the snap `theme.accent` inside json-render catalog components.
15
+ */
16
+ export function useSnapAccentScopeStyle(): CSSProperties {
17
+ const { get } = useStateStore();
18
+ const { mode } = useColorMode();
19
+ const pageAccent = useSnapPreviewPageAccent();
20
+ const fromState = get("/theme/accent");
21
+ const accentRaw =
22
+ (typeof pageAccent === "string" && pageAccent.length > 0
23
+ ? pageAccent
24
+ : fromState) ?? undefined;
25
+ const accentName =
26
+ typeof accentRaw === "string" && accentRaw.length > 0
27
+ ? accentRaw
28
+ : "purple";
29
+ return useMemo(
30
+ () => snapPreviewPrimaryCssProperties(accentName, mode),
31
+ [accentName, mode],
32
+ );
33
+ }
34
+
35
+ /** Active snap palette table for the current docs shell theme. */
36
+ export function useSnapPalette(): {
37
+ hex: (name: string) => string;
38
+ map: Record<PaletteColor, string>;
39
+ theme: "light" | "dark";
40
+ } {
41
+ const { mode } = useColorMode();
42
+ const map = mode === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
43
+ const hex = (name: string) => resolveSnapPaletteHex(name, mode);
44
+ return { hex, map, theme: mode };
45
+ }