@farcaster/snap 1.6.0 → 1.7.1

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 (65) hide show
  1. package/dist/dataStore.d.ts +1 -4
  2. package/dist/dataStore.js +4 -17
  3. package/dist/index.d.ts +1 -1
  4. package/dist/react-native/catalog-renderer.d.ts +5 -0
  5. package/dist/react-native/catalog-renderer.js +36 -0
  6. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  7. package/dist/react-native/components/snap-action-button.js +68 -0
  8. package/dist/react-native/components/snap-badge.d.ts +2 -0
  9. package/dist/react-native/components/snap-badge.js +38 -0
  10. package/dist/react-native/components/snap-icon.d.ts +5 -0
  11. package/dist/react-native/components/snap-icon.js +56 -0
  12. package/dist/react-native/components/snap-image.d.ts +2 -0
  13. package/dist/react-native/components/snap-image.js +24 -0
  14. package/dist/react-native/components/snap-input.d.ts +2 -0
  15. package/dist/react-native/components/snap-input.js +36 -0
  16. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  17. package/dist/react-native/components/snap-item-group.js +23 -0
  18. package/dist/react-native/components/snap-item.d.ts +5 -0
  19. package/dist/react-native/components/snap-item.js +45 -0
  20. package/dist/react-native/components/snap-progress.d.ts +2 -0
  21. package/dist/react-native/components/snap-progress.js +26 -0
  22. package/dist/react-native/components/snap-separator.d.ts +2 -0
  23. package/dist/react-native/components/snap-separator.js +23 -0
  24. package/dist/react-native/components/snap-slider.d.ts +2 -0
  25. package/dist/react-native/components/snap-slider.js +42 -0
  26. package/dist/react-native/components/snap-stack.d.ts +5 -0
  27. package/dist/react-native/components/snap-stack.js +49 -0
  28. package/dist/react-native/components/snap-switch.d.ts +2 -0
  29. package/dist/react-native/components/snap-switch.js +30 -0
  30. package/dist/react-native/components/snap-text.d.ts +2 -0
  31. package/dist/react-native/components/snap-text.js +37 -0
  32. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  33. package/dist/react-native/components/snap-toggle-group.js +100 -0
  34. package/dist/react-native/index.d.ts +52 -0
  35. package/dist/react-native/index.js +155 -0
  36. package/dist/react-native/theme.d.ts +21 -0
  37. package/dist/react-native/theme.js +37 -0
  38. package/dist/react-native/use-snap-palette.d.ts +13 -0
  39. package/dist/react-native/use-snap-palette.js +48 -0
  40. package/dist/ui/badge.d.ts +2 -2
  41. package/dist/ui/button.d.ts +2 -2
  42. package/dist/ui/catalog.d.ts +7 -7
  43. package/dist/ui/icon.d.ts +2 -2
  44. package/dist/ui/schema.d.ts +1 -1
  45. package/package.json +7 -2
  46. package/src/dataStore.ts +5 -29
  47. package/src/index.ts +0 -1
  48. package/src/react-native/catalog-renderer.tsx +37 -0
  49. package/src/react-native/components/snap-action-button.tsx +92 -0
  50. package/src/react-native/components/snap-badge.tsx +57 -0
  51. package/src/react-native/components/snap-icon.tsx +102 -0
  52. package/src/react-native/components/snap-image.tsx +38 -0
  53. package/src/react-native/components/snap-input.tsx +57 -0
  54. package/src/react-native/components/snap-item-group.tsx +43 -0
  55. package/src/react-native/components/snap-item.tsx +70 -0
  56. package/src/react-native/components/snap-progress.tsx +40 -0
  57. package/src/react-native/components/snap-separator.tsx +32 -0
  58. package/src/react-native/components/snap-slider.tsx +82 -0
  59. package/src/react-native/components/snap-stack.tsx +66 -0
  60. package/src/react-native/components/snap-switch.tsx +45 -0
  61. package/src/react-native/components/snap-text.tsx +53 -0
  62. package/src/react-native/components/snap-toggle-group.tsx +128 -0
  63. package/src/react-native/index.tsx +267 -0
  64. package/src/react-native/theme.tsx +73 -0
  65. package/src/react-native/use-snap-palette.ts +64 -0
package/src/dataStore.ts CHANGED
@@ -6,18 +6,14 @@ export type DataStoreValue =
6
6
  | DataStoreValue[]
7
7
  | { [key: string]: DataStoreValue };
8
8
 
9
- export type SnapDataStoreOperations = {
9
+ export type SnapDataStore = {
10
10
  get(key: string): Promise<DataStoreValue | null>;
11
11
  set(key: string, value: DataStoreValue): Promise<void>;
12
12
  };
13
13
 
14
- export type SnapDataStore = SnapDataStoreOperations & {
15
- withLock<T>(fn: (store: SnapDataStoreOperations) => Promise<T>): Promise<T>;
16
- };
17
-
18
14
  export function createDefaultDataStore(): SnapDataStore {
19
15
  const err = new Error(
20
- "Data store is not configured. Use withUpstash() from @farcaster/snap-upstash or provide a data store implementation.",
16
+ "Data store is not configured. Use withTursoServerless() from @farcaster/snap-turso or provide a data store implementation.",
21
17
  );
22
18
  return {
23
19
  get(_key: string): Promise<never> {
@@ -26,37 +22,17 @@ export function createDefaultDataStore(): SnapDataStore {
26
22
  set(_key: string, _value: DataStoreValue): Promise<never> {
27
23
  return Promise.reject(err);
28
24
  },
29
- withLock<T>(
30
- _fn: (store: SnapDataStoreOperations) => Promise<T>,
31
- ): Promise<never> {
32
- return Promise.reject(err);
33
- },
34
25
  };
35
26
  }
36
27
 
37
28
  export function createInMemoryDataStore(): SnapDataStore {
38
29
  const data = new Map<string, DataStoreValue>();
39
- const ops: SnapDataStoreOperations = {
40
- get: async (key: string): Promise<DataStoreValue | null> => {
30
+ return {
31
+ async get(key: string): Promise<DataStoreValue | null> {
41
32
  return data.get(key) ?? null;
42
33
  },
43
- set: async (key: string, value: DataStoreValue): Promise<void> => {
34
+ async set(key: string, value: DataStoreValue): Promise<void> {
44
35
  data.set(key, value);
45
36
  },
46
37
  };
47
- /** Serializes `withLock` callbacks so async work does not interleave across callers. */
48
- let lockChain: Promise<unknown> = Promise.resolve();
49
- return {
50
- ...ops,
51
- withLock<T>(
52
- fn: (store: SnapDataStoreOperations) => Promise<T>,
53
- ): Promise<T> {
54
- const run = lockChain.then(() => fn(ops));
55
- lockChain = run.then(
56
- () => undefined,
57
- () => undefined,
58
- );
59
- return run;
60
- },
61
- };
62
38
  }
package/src/index.ts CHANGED
@@ -32,7 +32,6 @@ export {
32
32
  export {
33
33
  type DataStoreValue,
34
34
  type SnapDataStore,
35
- type SnapDataStoreOperations,
36
35
  createDefaultDataStore,
37
36
  createInMemoryDataStore,
38
37
  } from "./dataStore";
@@ -0,0 +1,37 @@
1
+ import { createRenderer } from "@json-render/react-native";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapActionButton } from "./components/snap-action-button";
4
+ import { SnapBadge } from "./components/snap-badge";
5
+ import { SnapIcon } from "./components/snap-icon";
6
+ import { SnapImage } from "./components/snap-image";
7
+ import { SnapInput } from "./components/snap-input";
8
+ import { SnapItem } from "./components/snap-item";
9
+ import { SnapItemGroup } from "./components/snap-item-group";
10
+ import { SnapProgress } from "./components/snap-progress";
11
+ import { SnapSeparator } from "./components/snap-separator";
12
+ import { SnapSlider } from "./components/snap-slider";
13
+ import { SnapStack } from "./components/snap-stack";
14
+ import { SnapSwitch } from "./components/snap-switch";
15
+ import { SnapText } from "./components/snap-text";
16
+ import { SnapToggleGroup } from "./components/snap-toggle-group";
17
+
18
+ /**
19
+ * Maps snap json-render catalog types to React Native primitives.
20
+ * Keys match the snap wire-format `type` strings exactly (snake_case).
21
+ */
22
+ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
23
+ badge: SnapBadge,
24
+ button: SnapActionButton,
25
+ icon: SnapIcon,
26
+ image: SnapImage,
27
+ input: SnapInput,
28
+ item: SnapItem,
29
+ item_group: SnapItemGroup,
30
+ progress: SnapProgress,
31
+ separator: SnapSeparator,
32
+ slider: SnapSlider,
33
+ stack: SnapStack,
34
+ switch: SnapSwitch,
35
+ text: SnapText,
36
+ toggle_group: SnapToggleGroup,
37
+ });
@@ -0,0 +1,92 @@
1
+ declare const __DEV__: boolean;
2
+
3
+ import type { ComponentRenderProps } from "@json-render/react-native";
4
+ import { Pressable, StyleSheet, Text, View } from "react-native";
5
+ import { useSnapPalette } from "../use-snap-palette";
6
+ import { useSnapTheme } from "../theme";
7
+ import { ICON_MAP } from "./snap-icon";
8
+
9
+ const VARIANT_MAP: Record<string, "default" | "secondary" | "outline" | "ghost"> = {
10
+ default: "default",
11
+ secondary: "secondary",
12
+ outline: "outline",
13
+ ghost: "ghost",
14
+ };
15
+
16
+ export function SnapActionButton({
17
+ element: { props },
18
+ emit,
19
+ }: ComponentRenderProps<Record<string, unknown>>) {
20
+ const { accentHex } = useSnapPalette();
21
+ const { colors } = useSnapTheme();
22
+ const label = String(props.label ?? "Action");
23
+ const variant = VARIANT_MAP[String(props.variant ?? "default")] ?? "default";
24
+ const iconName = props.icon ? String(props.icon) : undefined;
25
+
26
+ const variantStyle = (() => {
27
+ switch (variant) {
28
+ case "default":
29
+ return { backgroundColor: accentHex };
30
+ case "secondary":
31
+ return { backgroundColor: "transparent", borderWidth: 1.5, borderColor: accentHex };
32
+ case "outline":
33
+ return { backgroundColor: "rgba(255,255,255,0.04)", borderWidth: 1, borderColor: colors.border };
34
+ case "ghost":
35
+ return { backgroundColor: "transparent" };
36
+ }
37
+ })();
38
+
39
+ const textColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
40
+ const iconColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
41
+
42
+ return (
43
+ <View style={styles.outer}>
44
+ <Pressable
45
+ style={({ pressed }) => [
46
+ styles.btn,
47
+ variant === "default" ? styles.btnDefault : styles.btnOther,
48
+ variantStyle,
49
+ pressed && styles.pressed,
50
+ ]}
51
+ onPress={() => {
52
+ void (async () => {
53
+ try {
54
+ await emit("press");
55
+ } catch (err: unknown) {
56
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
57
+ // eslint-disable-next-line no-console
58
+ console.error("[snap] action failed", err);
59
+ }
60
+ }
61
+ })();
62
+ }}
63
+ >
64
+ {iconName && ICON_MAP[iconName] ? (
65
+ (() => { const I = ICON_MAP[iconName]!; return <I size={16} color={iconColor} />; })()
66
+ ) : null}
67
+ <Text style={{ color: textColor, fontSize: 14, fontWeight: "600" }}>
68
+ {label}
69
+ </Text>
70
+ </Pressable>
71
+ </View>
72
+ );
73
+ }
74
+
75
+ const styles = StyleSheet.create({
76
+ outer: { flex: 1, minWidth: 0 },
77
+ btn: {
78
+ paddingHorizontal: 16,
79
+ borderRadius: 10,
80
+ alignItems: "center",
81
+ justifyContent: "center",
82
+ flexDirection: "row",
83
+ gap: 8,
84
+ },
85
+ btnDefault: {
86
+ paddingVertical: 10,
87
+ },
88
+ btnOther: {
89
+ paddingVertical: 8,
90
+ },
91
+ pressed: { opacity: 0.88 },
92
+ });
@@ -0,0 +1,57 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette";
4
+ import { ICON_MAP } from "./snap-icon";
5
+
6
+ export function SnapBadge({
7
+ element: { props },
8
+ }: ComponentRenderProps<Record<string, unknown>>) {
9
+ const { accentHex, hex } = useSnapPalette();
10
+ const label = String(props.label ?? "");
11
+ const color = props.color ? String(props.color) : undefined;
12
+ const iconName = props.icon ? String(props.icon) : undefined;
13
+ const isAccent = !color || color === "accent";
14
+ const resolvedColor = isAccent ? accentHex : hex(color);
15
+
16
+ const Icon = iconName ? ICON_MAP[iconName] : undefined;
17
+
18
+ return (
19
+ <View
20
+ style={[
21
+ styles.badge,
22
+ isAccent
23
+ ? { backgroundColor: resolvedColor, borderColor: resolvedColor }
24
+ : { borderColor: resolvedColor },
25
+ ]}
26
+ >
27
+ {Icon && (
28
+ <Icon size={12} color={isAccent ? "#fff" : resolvedColor} />
29
+ )}
30
+ <Text
31
+ style={[
32
+ styles.label,
33
+ { color: isAccent ? "#fff" : resolvedColor },
34
+ ]}
35
+ >
36
+ {label}
37
+ </Text>
38
+ </View>
39
+ );
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ badge: {
44
+ alignSelf: "flex-start",
45
+ flexDirection: "row",
46
+ alignItems: "center",
47
+ gap: 4,
48
+ paddingHorizontal: 8,
49
+ paddingVertical: 2,
50
+ borderRadius: 9999,
51
+ borderWidth: 1,
52
+ },
53
+ label: {
54
+ fontSize: 12,
55
+ fontWeight: "500",
56
+ },
57
+ });
@@ -0,0 +1,102 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette";
4
+ import {
5
+ ArrowRight,
6
+ ArrowLeft,
7
+ ExternalLink,
8
+ ChevronRight,
9
+ Check,
10
+ X,
11
+ AlertTriangle,
12
+ Info,
13
+ Clock,
14
+ Heart,
15
+ MessageCircle,
16
+ Repeat,
17
+ Share,
18
+ User,
19
+ Users,
20
+ Star,
21
+ Trophy,
22
+ Zap,
23
+ Flame,
24
+ Gift,
25
+ ImageIcon,
26
+ Play,
27
+ Pause,
28
+ Wallet,
29
+ Coins,
30
+ Plus,
31
+ Minus,
32
+ RefreshCw,
33
+ Bookmark,
34
+ ThumbsUp,
35
+ ThumbsDown,
36
+ TrendingUp,
37
+ TrendingDown,
38
+ type LucideIcon,
39
+ } from "lucide-react-native";
40
+
41
+ const ICON_MAP: Record<string, LucideIcon> = {
42
+ "arrow-right": ArrowRight,
43
+ "arrow-left": ArrowLeft,
44
+ "external-link": ExternalLink,
45
+ "chevron-right": ChevronRight,
46
+ check: Check,
47
+ x: X,
48
+ "alert-triangle": AlertTriangle,
49
+ info: Info,
50
+ clock: Clock,
51
+ heart: Heart,
52
+ "message-circle": MessageCircle,
53
+ repeat: Repeat,
54
+ share: Share,
55
+ user: User,
56
+ users: Users,
57
+ star: Star,
58
+ trophy: Trophy,
59
+ zap: Zap,
60
+ flame: Flame,
61
+ gift: Gift,
62
+ image: ImageIcon,
63
+ play: Play,
64
+ pause: Pause,
65
+ wallet: Wallet,
66
+ coins: Coins,
67
+ plus: Plus,
68
+ minus: Minus,
69
+ "refresh-cw": RefreshCw,
70
+ bookmark: Bookmark,
71
+ "thumbs-up": ThumbsUp,
72
+ "thumbs-down": ThumbsDown,
73
+ "trending-up": TrendingUp,
74
+ "trending-down": TrendingDown,
75
+ };
76
+
77
+ const SIZE_PX: Record<string, number> = {
78
+ sm: 16,
79
+ md: 20,
80
+ };
81
+
82
+ export function SnapIcon({
83
+ element: { props },
84
+ }: ComponentRenderProps<Record<string, unknown>>) {
85
+ const { accentHex, hex } = useSnapPalette();
86
+ const name = String(props.name ?? "info");
87
+ const size = SIZE_PX[String(props.size ?? "md")] ?? 20;
88
+ const color = props.color ? String(props.color) : undefined;
89
+ const isAccent = !color || color === "accent";
90
+ const resolvedColor = isAccent ? accentHex : hex(color);
91
+
92
+ const Icon = ICON_MAP[name];
93
+ if (!Icon) return null;
94
+
95
+ return (
96
+ <View style={{ alignItems: "center", justifyContent: "center" }}>
97
+ <Icon size={size} color={resolvedColor} />
98
+ </View>
99
+ );
100
+ }
101
+
102
+ export { ICON_MAP };
@@ -0,0 +1,38 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { Image } from "expo-image";
3
+ import { StyleSheet, View } from "react-native";
4
+
5
+ function aspectToRatio(aspect: string): number {
6
+ const [w, h] = aspect.split(":").map(Number);
7
+ if (!w || !h) return 1;
8
+ return w / h;
9
+ }
10
+
11
+ export function SnapImage({
12
+ element: { props },
13
+ }: ComponentRenderProps<Record<string, unknown>>) {
14
+ const url = String(props.url ?? "");
15
+ const alt = String(props.alt ?? "");
16
+ const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
17
+
18
+ return (
19
+ <View style={[styles.frame, { aspectRatio: ratio }]}>
20
+ <Image
21
+ source={{ uri: url }}
22
+ style={StyleSheet.absoluteFill}
23
+ contentFit="cover"
24
+ accessibilityLabel={alt || undefined}
25
+ />
26
+ </View>
27
+ );
28
+ }
29
+
30
+ const styles = StyleSheet.create({
31
+ frame: {
32
+ flex: 1,
33
+ width: "100%",
34
+ borderRadius: 8,
35
+ overflow: "hidden",
36
+ backgroundColor: "#f3f4f6",
37
+ },
38
+ });
@@ -0,0 +1,57 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { useStateStore } from "@json-render/react-native";
3
+ import { StyleSheet, Text, TextInput, View } from "react-native";
4
+ import { useSnapTheme } from "../theme";
5
+
6
+ export function SnapInput({
7
+ element: { props },
8
+ }: ComponentRenderProps<Record<string, unknown>>) {
9
+ const { get, set } = useStateStore();
10
+ const { colors } = useSnapTheme();
11
+ const name = String(props.name ?? "input");
12
+ const path = `/inputs/${name}`;
13
+ const label = props.label ? String(props.label) : undefined;
14
+ const placeholder = props.placeholder ? String(props.placeholder) : undefined;
15
+ const type = String(props.type ?? "text");
16
+ const maxLength =
17
+ typeof props.maxLength === "number" ? props.maxLength : undefined;
18
+ const defaultValue = props.defaultValue != null ? String(props.defaultValue) : "";
19
+ const raw = get(path);
20
+ const value = raw !== undefined && raw !== null ? String(raw) : defaultValue;
21
+
22
+ return (
23
+ <View style={styles.wrap}>
24
+ {label ? <Text style={[styles.label, { color: colors.text }]}>{label}</Text> : null}
25
+ <TextInput
26
+ style={[
27
+ styles.input,
28
+ {
29
+ borderColor: colors.border,
30
+ backgroundColor: colors.inputBg,
31
+ color: colors.text,
32
+ },
33
+ ]}
34
+ value={value}
35
+ onChangeText={(text) => set(path, type === "number" ? Number(text) || 0 : text)}
36
+ placeholder={placeholder}
37
+ placeholderTextColor={colors.textSecondary}
38
+ maxLength={maxLength}
39
+ autoCapitalize="none"
40
+ autoCorrect={false}
41
+ keyboardType={type === "number" ? "numeric" : "default"}
42
+ />
43
+ </View>
44
+ );
45
+ }
46
+
47
+ const styles = StyleSheet.create({
48
+ wrap: { width: "100%", gap: 4 },
49
+ label: { fontSize: 13, fontWeight: "500" },
50
+ input: {
51
+ borderWidth: 1,
52
+ borderRadius: 8,
53
+ paddingHorizontal: 12,
54
+ paddingVertical: 10,
55
+ fontSize: 14,
56
+ },
57
+ });
@@ -0,0 +1,43 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { Children, Fragment, type ReactNode } from "react";
3
+ import { StyleSheet, View } from "react-native";
4
+ import { useSnapTheme } from "../theme";
5
+
6
+ const GAP_MAP: Record<string, number> = { none: 0, sm: 4, md: 8, lg: 12 };
7
+
8
+ export function SnapItemGroup({
9
+ element: { props },
10
+ children,
11
+ }: ComponentRenderProps<Record<string, unknown>> & { children?: ReactNode }) {
12
+ const { colors } = useSnapTheme();
13
+ const border = Boolean(props.border);
14
+ const separator = Boolean(props.separator);
15
+ const gap = GAP_MAP[String(props.gap ?? "sm")] ?? 4;
16
+ const items = Children.toArray(children);
17
+
18
+ return (
19
+ <View
20
+ style={[
21
+ styles.group,
22
+ border && { borderWidth: 1, borderColor: colors.border, borderRadius: 12 },
23
+ { gap },
24
+ ]}
25
+ >
26
+ {items.map((child, i) => (
27
+ <Fragment key={i}>
28
+ {separator && i > 0 && (
29
+ <View style={{ height: 1, backgroundColor: colors.border + "80" }} />
30
+ )}
31
+ {child}
32
+ </Fragment>
33
+ ))}
34
+ </View>
35
+ );
36
+ }
37
+
38
+ const styles = StyleSheet.create({
39
+ group: {
40
+ width: "100%",
41
+ overflow: "hidden",
42
+ },
43
+ });
@@ -0,0 +1,70 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import type { ReactNode } from "react";
3
+ import { StyleSheet, Text, View } from "react-native";
4
+ import { useSnapTheme } from "../theme";
5
+
6
+ export function SnapItem({
7
+ element: { props },
8
+ children,
9
+ }: ComponentRenderProps<Record<string, unknown>> & { children?: ReactNode }) {
10
+ const { colors } = useSnapTheme();
11
+ const title = String(props.title ?? "");
12
+ const description = props.description
13
+ ? String(props.description)
14
+ : undefined;
15
+ const variant = String(props.variant ?? "default");
16
+
17
+ const containerVariant =
18
+ variant === "outline"
19
+ ? { borderWidth: 1, borderColor: colors.border + "80", borderRadius: 8, padding: 10 }
20
+ : variant === "muted"
21
+ ? { backgroundColor: "rgba(255,255,255,0.04)", borderRadius: 8, padding: 10 }
22
+ : { paddingVertical: 8, paddingHorizontal: 10 };
23
+
24
+ return (
25
+ <View style={[styles.container, containerVariant]}>
26
+ <View style={styles.content}>
27
+ <Text style={[styles.title, { color: colors.text }]}>{title}</Text>
28
+ {description ? (
29
+ <Text style={[styles.description, { color: colors.textSecondary }]}>
30
+ {description}
31
+ </Text>
32
+ ) : null}
33
+ </View>
34
+ {children ? (
35
+ <View style={styles.actions}>
36
+ <View style={{ flex: 0 }}>{children}</View>
37
+ </View>
38
+ ) : null}
39
+ </View>
40
+ );
41
+ }
42
+
43
+ const styles = StyleSheet.create({
44
+ container: {
45
+ flex: 1,
46
+ flexDirection: "row",
47
+ alignItems: "center",
48
+ },
49
+ content: {
50
+ flex: 1,
51
+ },
52
+ title: {
53
+ fontSize: 15,
54
+ fontWeight: "500",
55
+ },
56
+ description: {
57
+ fontSize: 13,
58
+ marginTop: 1,
59
+ },
60
+ actions: {
61
+ marginLeft: "auto",
62
+ paddingLeft: 12,
63
+ flexDirection: "row",
64
+ alignItems: "center",
65
+ flexShrink: 0,
66
+ flexGrow: 0,
67
+ flexBasis: "auto",
68
+ gap: 4,
69
+ },
70
+ });
@@ -0,0 +1,40 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette";
4
+ import { useSnapTheme } from "../theme";
5
+
6
+ export function SnapProgress({
7
+ element: { props },
8
+ }: ComponentRenderProps<Record<string, unknown>>) {
9
+ const { accentHex } = useSnapPalette();
10
+ const { colors } = useSnapTheme();
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 != null ? String(props.label) : null;
15
+
16
+ return (
17
+ <View style={styles.wrap}>
18
+ {label ? (
19
+ <Text style={[styles.label, { color: colors.textSecondary }]}>{label}</Text>
20
+ ) : null}
21
+ <View style={[styles.track, { backgroundColor: colors.muted }]}>
22
+ <View style={[styles.fill, { width: `${percent}%`, backgroundColor: accentHex }]} />
23
+ </View>
24
+ </View>
25
+ );
26
+ }
27
+
28
+ const styles = StyleSheet.create({
29
+ wrap: { flex: 1, width: "100%", gap: 4 },
30
+ label: { fontSize: 13 },
31
+ track: {
32
+ height: 10,
33
+ borderRadius: 9999,
34
+ overflow: "hidden",
35
+ },
36
+ fill: {
37
+ height: "100%",
38
+ borderRadius: 9999,
39
+ },
40
+ });
@@ -0,0 +1,32 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { StyleSheet, View } from "react-native";
3
+ import { useSnapTheme } from "../theme";
4
+
5
+ export function SnapSeparator({
6
+ element: { props },
7
+ }: ComponentRenderProps<Record<string, unknown>>) {
8
+ const { colors } = useSnapTheme();
9
+ const orientation = String(props.orientation ?? "horizontal");
10
+ const isVertical = orientation === "vertical";
11
+
12
+ return (
13
+ <View
14
+ style={[
15
+ isVertical ? styles.vertical : styles.horizontal,
16
+ { backgroundColor: colors.border + "80" },
17
+ ]}
18
+ />
19
+ );
20
+ }
21
+
22
+ const styles = StyleSheet.create({
23
+ horizontal: {
24
+ width: "100%",
25
+ height: 1,
26
+ },
27
+ vertical: {
28
+ height: "100%",
29
+ width: 1,
30
+ alignSelf: "stretch",
31
+ },
32
+ });