@farcaster/snap 1.6.0 → 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 (60) hide show
  1. package/dist/react-native/catalog-renderer.d.ts +5 -0
  2. package/dist/react-native/catalog-renderer.js +36 -0
  3. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  4. package/dist/react-native/components/snap-action-button.js +68 -0
  5. package/dist/react-native/components/snap-badge.d.ts +2 -0
  6. package/dist/react-native/components/snap-badge.js +38 -0
  7. package/dist/react-native/components/snap-icon.d.ts +5 -0
  8. package/dist/react-native/components/snap-icon.js +56 -0
  9. package/dist/react-native/components/snap-image.d.ts +2 -0
  10. package/dist/react-native/components/snap-image.js +24 -0
  11. package/dist/react-native/components/snap-input.d.ts +2 -0
  12. package/dist/react-native/components/snap-input.js +36 -0
  13. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  14. package/dist/react-native/components/snap-item-group.js +23 -0
  15. package/dist/react-native/components/snap-item.d.ts +5 -0
  16. package/dist/react-native/components/snap-item.js +45 -0
  17. package/dist/react-native/components/snap-progress.d.ts +2 -0
  18. package/dist/react-native/components/snap-progress.js +26 -0
  19. package/dist/react-native/components/snap-separator.d.ts +2 -0
  20. package/dist/react-native/components/snap-separator.js +23 -0
  21. package/dist/react-native/components/snap-slider.d.ts +2 -0
  22. package/dist/react-native/components/snap-slider.js +42 -0
  23. package/dist/react-native/components/snap-stack.d.ts +5 -0
  24. package/dist/react-native/components/snap-stack.js +49 -0
  25. package/dist/react-native/components/snap-switch.d.ts +2 -0
  26. package/dist/react-native/components/snap-switch.js +30 -0
  27. package/dist/react-native/components/snap-text.d.ts +2 -0
  28. package/dist/react-native/components/snap-text.js +37 -0
  29. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  30. package/dist/react-native/components/snap-toggle-group.js +100 -0
  31. package/dist/react-native/index.d.ts +52 -0
  32. package/dist/react-native/index.js +155 -0
  33. package/dist/react-native/theme.d.ts +21 -0
  34. package/dist/react-native/theme.js +37 -0
  35. package/dist/react-native/use-snap-palette.d.ts +13 -0
  36. package/dist/react-native/use-snap-palette.js +48 -0
  37. package/dist/ui/badge.d.ts +2 -2
  38. package/dist/ui/button.d.ts +2 -2
  39. package/dist/ui/catalog.d.ts +7 -7
  40. package/dist/ui/icon.d.ts +2 -2
  41. package/dist/ui/schema.d.ts +1 -1
  42. package/package.json +7 -2
  43. package/src/react-native/catalog-renderer.tsx +37 -0
  44. package/src/react-native/components/snap-action-button.tsx +92 -0
  45. package/src/react-native/components/snap-badge.tsx +57 -0
  46. package/src/react-native/components/snap-icon.tsx +102 -0
  47. package/src/react-native/components/snap-image.tsx +38 -0
  48. package/src/react-native/components/snap-input.tsx +57 -0
  49. package/src/react-native/components/snap-item-group.tsx +43 -0
  50. package/src/react-native/components/snap-item.tsx +70 -0
  51. package/src/react-native/components/snap-progress.tsx +40 -0
  52. package/src/react-native/components/snap-separator.tsx +32 -0
  53. package/src/react-native/components/snap-slider.tsx +82 -0
  54. package/src/react-native/components/snap-stack.tsx +66 -0
  55. package/src/react-native/components/snap-switch.tsx +45 -0
  56. package/src/react-native/components/snap-text.tsx +53 -0
  57. package/src/react-native/components/snap-toggle-group.tsx +128 -0
  58. package/src/react-native/index.tsx +267 -0
  59. package/src/react-native/theme.tsx +73 -0
  60. package/src/react-native/use-snap-palette.ts +64 -0
@@ -0,0 +1,45 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { useStateStore } from "@json-render/react-native";
3
+ import { StyleSheet, Switch, Text, View } from "react-native";
4
+ import { useSnapPalette } from "../use-snap-palette";
5
+ import { useSnapTheme } from "../theme";
6
+
7
+ export function SnapSwitch({
8
+ element: { props },
9
+ }: ComponentRenderProps<Record<string, unknown>>) {
10
+ const { get, set } = useStateStore();
11
+ const { accentHex } = useSnapPalette();
12
+ const { colors } = useSnapTheme();
13
+ const name = String(props.name ?? "switch");
14
+ const path = `/inputs/${name}`;
15
+ const label = props.label ? String(props.label) : undefined;
16
+ const fallback = Boolean(props.defaultChecked ?? false);
17
+ const raw = get(path);
18
+ const checked = raw === undefined || raw === null ? fallback : Boolean(raw);
19
+
20
+ return (
21
+ <View style={styles.row}>
22
+ {label ? <Text style={[styles.label, { color: colors.text }]}>{label}</Text> : null}
23
+ <Switch
24
+ value={checked}
25
+ onValueChange={(v) => set(path, v)}
26
+ trackColor={{ false: colors.border, true: accentHex }}
27
+ thumbColor="#fff"
28
+ />
29
+ </View>
30
+ );
31
+ }
32
+
33
+ const styles = StyleSheet.create({
34
+ row: {
35
+ flexDirection: "row",
36
+ alignItems: "center",
37
+ justifyContent: "space-between",
38
+ gap: 12,
39
+ },
40
+ label: {
41
+ fontSize: 14,
42
+ fontWeight: "400",
43
+ flex: 1,
44
+ },
45
+ });
@@ -0,0 +1,53 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapTheme } from "../theme";
4
+
5
+ const SIZE_STYLES: Record<string, { fontSize: number; lineHeight?: number; fontWeight?: "400" | "500" | "600" | "700" }> = {
6
+ lg: { fontSize: 20, fontWeight: "700" },
7
+ md: { fontSize: 16, lineHeight: 24 },
8
+ sm: { fontSize: 13 },
9
+ };
10
+
11
+ const WEIGHT_MAP: Record<string, "400" | "500" | "600" | "700"> = {
12
+ bold: "700",
13
+ medium: "500",
14
+ normal: "400",
15
+ };
16
+
17
+ export function SnapText({
18
+ element: { props },
19
+ }: ComponentRenderProps<Record<string, unknown>>) {
20
+ const { colors } = useSnapTheme();
21
+ const content = String(props.content ?? "");
22
+ const size = String(props.size ?? "md");
23
+ const weight = props.weight ? String(props.weight) : undefined;
24
+ const align = (props.align as "left" | "center" | "right" | undefined) ?? undefined;
25
+
26
+ const sizeStyle = SIZE_STYLES[size] ?? SIZE_STYLES.md;
27
+ const resolvedWeight = weight ? WEIGHT_MAP[weight] : sizeStyle?.fontWeight;
28
+ const textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
29
+
30
+ return (
31
+ <View style={styles.wrap}>
32
+ <Text
33
+ style={[
34
+ styles.base,
35
+ {
36
+ color: colors.text,
37
+ fontSize: sizeStyle!.fontSize,
38
+ lineHeight: sizeStyle!.lineHeight,
39
+ fontWeight: resolvedWeight,
40
+ textAlign,
41
+ },
42
+ ]}
43
+ >
44
+ {content}
45
+ </Text>
46
+ </View>
47
+ );
48
+ }
49
+
50
+ const styles = StyleSheet.create({
51
+ wrap: { flex: 1, width: "100%" },
52
+ base: {},
53
+ });
@@ -0,0 +1,128 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { useStateStore } from "@json-render/react-native";
3
+ import { Pressable, StyleSheet, Text, View } from "react-native";
4
+ import { useSnapPalette } from "../use-snap-palette";
5
+ import { useSnapTheme } from "../theme";
6
+
7
+ export function SnapToggleGroup({
8
+ element: { props },
9
+ }: ComponentRenderProps<Record<string, unknown>>) {
10
+ const { get, set } = useStateStore();
11
+ const { accentHex } = useSnapPalette();
12
+ const { colors } = useSnapTheme();
13
+ const name = String(props.name ?? "toggle_group");
14
+ const path = `/inputs/${name}`;
15
+ const label = props.label ? String(props.label) : undefined;
16
+ const isMultiple = Boolean(props.multiple);
17
+ const orientation = String(props.orientation ?? "horizontal");
18
+ const options = Array.isArray(props.options)
19
+ ? (props.options as string[])
20
+ : [];
21
+
22
+ const raw = get(path);
23
+ const defaultValue = props.defaultValue;
24
+
25
+ const selected = (() => {
26
+ if (raw !== undefined && raw !== null) {
27
+ return isMultiple
28
+ ? Array.isArray(raw)
29
+ ? (raw as string[])
30
+ : []
31
+ : typeof raw === "string"
32
+ ? [raw]
33
+ : [];
34
+ }
35
+ if (defaultValue !== undefined) {
36
+ return Array.isArray(defaultValue)
37
+ ? (defaultValue as string[])
38
+ : [String(defaultValue)];
39
+ }
40
+ return [];
41
+ })();
42
+
43
+ const isVertical = orientation === "vertical";
44
+
45
+ const handlePress = (opt: string) => {
46
+ if (isMultiple) {
47
+ const next = selected.includes(opt)
48
+ ? selected.filter((s) => s !== opt)
49
+ : [...selected, opt];
50
+ set(path, next);
51
+ } else {
52
+ if (opt && opt !== selected[0]) set(path, opt);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <View style={styles.wrap}>
58
+ {label ? <Text style={[styles.label, { color: colors.text }]}>{label}</Text> : null}
59
+ <View
60
+ style={[
61
+ styles.group,
62
+ { backgroundColor: colors.border + "33" },
63
+ isVertical ? styles.groupVertical : styles.groupHorizontal,
64
+ ]}
65
+ >
66
+ {options.map((opt, index) => {
67
+ const isSelected = selected.includes(opt);
68
+ return (
69
+ <Pressable
70
+ key={index}
71
+ style={({ pressed }) => [
72
+ styles.option,
73
+ isSelected && { backgroundColor: accentHex },
74
+ pressed && styles.pressed,
75
+ !isVertical && styles.optionHorizontal,
76
+ ]}
77
+ onPress={() => handlePress(opt)}
78
+ >
79
+ <Text
80
+ style={[
81
+ styles.optionText,
82
+ { color: colors.text },
83
+ isSelected && styles.optionTextSelected,
84
+ ]}
85
+ >
86
+ {opt}
87
+ </Text>
88
+ </Pressable>
89
+ );
90
+ })}
91
+ </View>
92
+ </View>
93
+ );
94
+ }
95
+
96
+ const styles = StyleSheet.create({
97
+ wrap: { width: "100%", gap: 6 },
98
+ label: { fontSize: 13, fontWeight: "500" },
99
+ group: {
100
+ padding: 4,
101
+ borderRadius: 8,
102
+ gap: 4,
103
+ },
104
+ groupHorizontal: {
105
+ flexDirection: "row",
106
+ },
107
+ groupVertical: {
108
+ flexDirection: "column",
109
+ },
110
+ option: {
111
+ paddingVertical: 8,
112
+ paddingHorizontal: 12,
113
+ borderRadius: 6,
114
+ alignItems: "center",
115
+ justifyContent: "center",
116
+ },
117
+ optionHorizontal: {
118
+ flex: 1,
119
+ },
120
+ pressed: { opacity: 0.88 },
121
+ optionText: {
122
+ fontSize: 13,
123
+ fontWeight: "500",
124
+ },
125
+ optionTextSelected: {
126
+ color: "#fff",
127
+ },
128
+ });
@@ -0,0 +1,267 @@
1
+ import type { Spec } from "@json-render/core";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapCatalogView } from "./catalog-renderer";
4
+ import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "./theme";
5
+ import { hexToRgba } from "./use-snap-palette";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import { ActivityIndicator, StyleSheet, View } from "react-native";
8
+ import {
9
+ DEFAULT_THEME_ACCENT,
10
+ PALETTE_LIGHT_HEX,
11
+ PALETTE_DARK_HEX,
12
+ type PaletteColor,
13
+ } from "@farcaster/snap";
14
+
15
+ // ─── Public types ──────────────────────────────────────
16
+
17
+ export type JsonValue =
18
+ | string
19
+ | number
20
+ | boolean
21
+ | null
22
+ | JsonValue[]
23
+ | { [key: string]: JsonValue };
24
+
25
+ export type SnapPage = {
26
+ version: string;
27
+ theme?: { accent?: string };
28
+ effects?: string[];
29
+ ui: Spec;
30
+ };
31
+
32
+ export type SnapActionHandlers = {
33
+ submit: (target: string, inputs: Record<string, JsonValue>) => void;
34
+ open_url: (target: string) => void;
35
+ open_mini_app: (target: string) => void;
36
+ view_cast: (params: { hash: string }) => void;
37
+ view_profile: (params: { fid: number }) => void;
38
+ compose_cast: (params: {
39
+ text?: string;
40
+ channelKey?: string;
41
+ embeds?: string[];
42
+ }) => void;
43
+ view_token: (params: { token: string }) => void;
44
+ send_token: (params: {
45
+ token: string;
46
+ amount?: string;
47
+ recipientFid?: number;
48
+ recipientAddress?: string;
49
+ }) => void;
50
+ swap_token: (params: {
51
+ sellToken?: string;
52
+ buyToken?: string;
53
+ }) => void;
54
+ };
55
+
56
+ // ─── Re-exports ───────────────────────────────────────
57
+
58
+ export { useSnapTheme, hexToRgba };
59
+ export type { SnapNativeColors };
60
+
61
+ // ─── Internal helpers ─────────────────────────────────
62
+
63
+ function applyStatePaths(
64
+ model: Record<string, unknown>,
65
+ changes: { path: string; value: unknown }[] | Record<string, unknown>,
66
+ ): void {
67
+ const entries = Array.isArray(changes)
68
+ ? changes.map((c) => [c.path, c.value] as const)
69
+ : Object.entries(changes);
70
+ for (const [path, value] of entries) {
71
+ const trimmed = path.startsWith("/") ? path : `/${path}`;
72
+ const parts = trimmed.split("/").filter(Boolean);
73
+ if (parts.length < 2) continue;
74
+ const [top, ...rest] = parts;
75
+ if (top === "inputs") {
76
+ if (typeof model.inputs !== "object" || model.inputs === null) {
77
+ model.inputs = {};
78
+ }
79
+ const inputs = model.inputs as Record<string, unknown>;
80
+ if (rest.length === 1) {
81
+ inputs[rest[0]!] = value;
82
+ }
83
+ continue;
84
+ }
85
+ if (top === "theme") {
86
+ if (typeof model.theme !== "object" || model.theme === null) {
87
+ model.theme = {};
88
+ }
89
+ const theme = model.theme as Record<string, unknown>;
90
+ if (rest.length === 1) {
91
+ theme[rest[0]!] = value;
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ function resolveAccentHex(accent: string | undefined, appearance: "light" | "dark"): string {
98
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
99
+ const name = accent && Object.hasOwn(map, accent) ? (accent as PaletteColor) : DEFAULT_THEME_ACCENT;
100
+ return map[name];
101
+ }
102
+
103
+ // ─── SnapView ─────────────────────────────────────────
104
+
105
+ function SnapViewInner({
106
+ snap,
107
+ handlers,
108
+ loading = false,
109
+ }: {
110
+ snap: SnapPage;
111
+ handlers: SnapActionHandlers;
112
+ loading?: boolean;
113
+ }) {
114
+ const { mode } = useSnapTheme();
115
+ const spec = snap.ui;
116
+ const accentHex = resolveAccentHex(snap.theme?.accent, mode);
117
+
118
+ const initialState = useMemo(
119
+ () => ({
120
+ ...(spec.state ?? {}),
121
+ inputs: { ...((spec.state?.inputs ?? {}) as Record<string, unknown>) },
122
+ theme: {
123
+ ...((spec.state?.theme ?? {}) as Record<string, unknown>),
124
+ ...(snap.theme ? { accent: snap.theme.accent } : {}),
125
+ },
126
+ }),
127
+ [spec, snap.theme],
128
+ );
129
+
130
+ const stateRef = useRef<Record<string, unknown>>(initialState);
131
+
132
+ useEffect(() => {
133
+ stateRef.current = {
134
+ inputs: {
135
+ ...((initialState.inputs ?? {}) as Record<string, unknown>),
136
+ },
137
+ theme: {
138
+ ...((initialState.theme ?? {}) as Record<string, unknown>),
139
+ },
140
+ };
141
+ }, [initialState]);
142
+
143
+ useEffect(() => {
144
+ const result = snapJsonRenderCatalog.validate(spec);
145
+ if (!result.success) {
146
+ // eslint-disable-next-line no-console
147
+ console.warn("[SnapView] catalog validation issues:", result.error);
148
+ }
149
+ }, [spec]);
150
+
151
+ const [pageKey, setPageKey] = useState(0);
152
+ useEffect(() => {
153
+ setPageKey((k) => k + 1);
154
+ }, [spec]);
155
+
156
+ const handlersRef = useRef(handlers);
157
+ handlersRef.current = handlers;
158
+
159
+ const handleAction = useCallback((name: unknown, params: unknown) => {
160
+ const inputs = (stateRef.current.inputs ?? {}) as Record<string, JsonValue>;
161
+ const p = (params ?? {}) as Record<string, unknown>;
162
+ const h = handlersRef.current;
163
+ switch (name) {
164
+ case "submit":
165
+ h.submit(String(p.target ?? ""), inputs);
166
+ break;
167
+ case "open_url":
168
+ h.open_url(String(p.target ?? ""));
169
+ break;
170
+ case "open_mini_app":
171
+ h.open_mini_app(String(p.target ?? ""));
172
+ break;
173
+ case "view_cast":
174
+ h.view_cast({ hash: String(p.hash ?? "") });
175
+ break;
176
+ case "view_profile":
177
+ h.view_profile({ fid: Number(p.fid ?? 0) });
178
+ break;
179
+ case "compose_cast":
180
+ h.compose_cast({
181
+ text: p.text ? String(p.text) : undefined,
182
+ channelKey: p.channelKey ? String(p.channelKey) : undefined,
183
+ embeds: Array.isArray(p.embeds) ? (p.embeds as string[]) : undefined,
184
+ });
185
+ break;
186
+ case "view_token":
187
+ h.view_token({ token: String(p.token ?? "") });
188
+ break;
189
+ case "send_token":
190
+ h.send_token({
191
+ token: String(p.token ?? ""),
192
+ amount: p.amount ? String(p.amount) : undefined,
193
+ recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
194
+ recipientAddress: p.recipientAddress ? String(p.recipientAddress) : undefined,
195
+ });
196
+ break;
197
+ case "swap_token":
198
+ h.swap_token({
199
+ sellToken: p.sellToken ? String(p.sellToken) : undefined,
200
+ buyToken: p.buyToken ? String(p.buyToken) : undefined,
201
+ });
202
+ break;
203
+ default:
204
+ break;
205
+ }
206
+ }, []);
207
+
208
+ return (
209
+ <View style={styles.container}>
210
+ {loading && (
211
+ <View
212
+ style={[
213
+ styles.overlay,
214
+ {
215
+ backgroundColor:
216
+ mode === "dark" ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.75)",
217
+ },
218
+ ]}
219
+ >
220
+ <ActivityIndicator size="large" color={accentHex} />
221
+ </View>
222
+ )}
223
+ <SnapCatalogView
224
+ key={pageKey}
225
+ spec={spec}
226
+ state={initialState}
227
+ loading={false}
228
+ onStateChange={(changes) => {
229
+ applyStatePaths(stateRef.current, changes);
230
+ }}
231
+ onAction={handleAction}
232
+ />
233
+ </View>
234
+ );
235
+ }
236
+
237
+ export function SnapView({
238
+ snap,
239
+ handlers,
240
+ loading = false,
241
+ appearance = "dark",
242
+ colors,
243
+ }: {
244
+ snap: SnapPage;
245
+ handlers: SnapActionHandlers;
246
+ loading?: boolean;
247
+ appearance?: "light" | "dark";
248
+ colors?: Partial<SnapNativeColors>;
249
+ }) {
250
+ return (
251
+ <SnapThemeProvider appearance={appearance} colors={colors}>
252
+ <SnapViewInner snap={snap} handlers={handlers} loading={loading} />
253
+ </SnapThemeProvider>
254
+ );
255
+ }
256
+
257
+ const styles = StyleSheet.create({
258
+ container: {
259
+ width: "100%",
260
+ },
261
+ overlay: {
262
+ ...StyleSheet.absoluteFillObject,
263
+ alignItems: "center",
264
+ justifyContent: "center",
265
+ zIndex: 10,
266
+ },
267
+ });
@@ -0,0 +1,73 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+
3
+ // ─── Color tokens ─────────────────────────────────────
4
+
5
+ export type SnapNativeColors = {
6
+ bg: string;
7
+ surface: string;
8
+ text: string;
9
+ textSecondary: string;
10
+ border: string;
11
+ inputBg: string;
12
+ muted: string;
13
+ };
14
+
15
+ const DEFAULT_LIGHT: SnapNativeColors = {
16
+ bg: "#dfe3e8",
17
+ surface: "#ffffff",
18
+ text: "#111111",
19
+ textSecondary: "#6b7280",
20
+ border: "#d1d5db",
21
+ inputBg: "#ffffff",
22
+ muted: "#f9fafb",
23
+ };
24
+
25
+ const DEFAULT_DARK: SnapNativeColors = {
26
+ bg: "#111318",
27
+ surface: "#1a1d24",
28
+ text: "#fafafa",
29
+ textSecondary: "#a1a1aa",
30
+ border: "#374151",
31
+ inputBg: "#1a1d24",
32
+ muted: "#27272a",
33
+ };
34
+
35
+ // ─── Context ──────────────────────────────────────────
36
+
37
+ interface SnapThemeValue {
38
+ mode: "light" | "dark";
39
+ colors: SnapNativeColors;
40
+ }
41
+
42
+ const SnapThemeContext = createContext<SnapThemeValue>({
43
+ mode: "dark",
44
+ colors: DEFAULT_DARK,
45
+ });
46
+
47
+ export function SnapThemeProvider({
48
+ appearance,
49
+ colors,
50
+ children,
51
+ }: {
52
+ appearance: "light" | "dark";
53
+ colors?: Partial<SnapNativeColors>;
54
+ children: ReactNode;
55
+ }) {
56
+ const value = useMemo<SnapThemeValue>(() => {
57
+ const defaults = appearance === "dark" ? DEFAULT_DARK : DEFAULT_LIGHT;
58
+ return {
59
+ mode: appearance,
60
+ colors: colors ? { ...defaults, ...colors } : defaults,
61
+ };
62
+ }, [appearance, colors]);
63
+
64
+ return (
65
+ <SnapThemeContext.Provider value={value}>
66
+ {children}
67
+ </SnapThemeContext.Provider>
68
+ );
69
+ }
70
+
71
+ export function useSnapTheme(): SnapThemeValue {
72
+ return useContext(SnapThemeContext);
73
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ DEFAULT_THEME_ACCENT,
3
+ PALETTE_COLOR_VALUES,
4
+ PALETTE_LIGHT_HEX,
5
+ PALETTE_DARK_HEX,
6
+ type PaletteColor,
7
+ } from "@farcaster/snap";
8
+ import { useStateStore } from "@json-render/react-native";
9
+ import { useSnapTheme } from "./theme";
10
+
11
+ function resolveHex(name: string, appearance: "light" | "dark"): string {
12
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
13
+ if (Object.hasOwn(map, name)) {
14
+ return map[name as PaletteColor];
15
+ }
16
+ return map.purple;
17
+ }
18
+
19
+ function isPaletteColor(s: string): s is PaletteColor {
20
+ return (PALETTE_COLOR_VALUES as readonly string[]).includes(s);
21
+ }
22
+
23
+ function themeAccentFromStore(get: (path: string) => unknown): PaletteColor {
24
+ const raw = get("/theme/accent");
25
+ if (typeof raw === "string" && isPaletteColor(raw)) {
26
+ return raw;
27
+ }
28
+ return DEFAULT_THEME_ACCENT;
29
+ }
30
+
31
+ export function useSnapPalette() {
32
+ const { mode } = useSnapTheme();
33
+ const { get } = useStateStore();
34
+ const accentName = themeAccentFromStore(get);
35
+ const accentHex = resolveHex(accentName, mode);
36
+
37
+ const hex = (semantic: string) =>
38
+ semantic === "accent" ? accentHex : resolveHex(semantic, mode);
39
+
40
+ return { appearance: mode, accentName, accentHex, hex };
41
+ }
42
+
43
+ /** `#RRGGBB` + alpha → `rgba(...)` for React Native styles. */
44
+ export function hexToRgba(hex: string, alpha: number): string {
45
+ const m = /^#([0-9a-fA-F]{6})$/.exec(hex.trim());
46
+ if (!m) {
47
+ return `rgba(0,0,0,${alpha})`;
48
+ }
49
+ const n = Number.parseInt(m[1]!, 16);
50
+ const r = (n >> 16) & 255;
51
+ const g = (n >> 8) & 255;
52
+ const b = n & 255;
53
+ return `rgba(${r},${g},${b},${alpha})`;
54
+ }
55
+
56
+ export function useSnapPreviewChromePalette(themeAccent: string | undefined) {
57
+ const { mode } = useSnapTheme();
58
+ const accentName =
59
+ typeof themeAccent === "string" && isPaletteColor(themeAccent)
60
+ ? themeAccent
61
+ : DEFAULT_THEME_ACCENT;
62
+ const accentHex = resolveHex(accentName, mode);
63
+ return { appearance: mode, accentName, accentHex };
64
+ }