@farcaster/snap 2.0.0 → 2.0.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 (194) hide show
  1. package/dist/colors.d.ts +4 -4
  2. package/dist/colors.js +20 -20
  3. package/dist/constants.d.ts +17 -1
  4. package/dist/constants.js +19 -1
  5. package/dist/index.d.ts +4 -6
  6. package/dist/index.js +2 -4
  7. package/dist/react/accent-context.d.ts +3 -1
  8. package/dist/react/accent-context.js +7 -4
  9. package/dist/react/catalog-renderer.js +4 -0
  10. package/dist/react/components/action-button.d.ts +2 -1
  11. package/dist/react/components/action-button.js +32 -13
  12. package/dist/react/components/badge.js +8 -8
  13. package/dist/react/components/bar-chart.d.ts +5 -0
  14. package/dist/react/components/bar-chart.js +26 -0
  15. package/dist/react/components/cell-grid.d.ts +5 -0
  16. package/dist/react/components/cell-grid.js +87 -0
  17. package/dist/react/components/icon.js +4 -10
  18. package/dist/react/components/input.js +12 -6
  19. package/dist/react/components/item-group.js +3 -1
  20. package/dist/react/components/item.d.ts +3 -3
  21. package/dist/react/components/item.js +4 -3
  22. package/dist/react/components/progress.js +3 -3
  23. package/dist/react/components/separator.js +3 -1
  24. package/dist/react/components/slider.js +15 -10
  25. package/dist/react/components/switch.js +10 -12
  26. package/dist/react/components/text.js +6 -14
  27. package/dist/react/components/toggle-group.js +20 -6
  28. package/dist/react/hooks/use-snap-colors.d.ts +38 -0
  29. package/dist/react/hooks/use-snap-colors.js +81 -0
  30. package/dist/react/index.d.ts +13 -1
  31. package/dist/react/index.js +9 -188
  32. package/dist/react/snap-view-core.d.ts +11 -0
  33. package/dist/react/snap-view-core.js +224 -0
  34. package/dist/react/v1/snap-view.d.ts +16 -0
  35. package/dist/react/v1/snap-view.js +90 -0
  36. package/dist/react/v2/snap-view.d.ts +23 -0
  37. package/dist/react/v2/snap-view.js +91 -0
  38. package/dist/react-native/catalog-renderer.d.ts +5 -0
  39. package/dist/react-native/catalog-renderer.js +40 -0
  40. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  41. package/dist/react-native/components/snap-action-button.js +69 -0
  42. package/dist/react-native/components/snap-badge.d.ts +2 -0
  43. package/dist/react-native/components/snap-badge.js +41 -0
  44. package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
  45. package/dist/react-native/components/snap-bar-chart.js +39 -0
  46. package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
  47. package/dist/react-native/components/snap-cell-grid.js +94 -0
  48. package/dist/react-native/components/snap-icon.d.ts +5 -0
  49. package/dist/react-native/components/snap-icon.js +56 -0
  50. package/dist/react-native/components/snap-image.d.ts +2 -0
  51. package/dist/react-native/components/snap-image.js +23 -0
  52. package/dist/react-native/components/snap-input.d.ts +2 -0
  53. package/dist/react-native/components/snap-input.js +37 -0
  54. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  55. package/dist/react-native/components/snap-item-group.js +23 -0
  56. package/dist/react-native/components/snap-item.d.ts +5 -0
  57. package/dist/react-native/components/snap-item.js +42 -0
  58. package/dist/react-native/components/snap-progress.d.ts +2 -0
  59. package/dist/react-native/components/snap-progress.js +26 -0
  60. package/dist/react-native/components/snap-separator.d.ts +2 -0
  61. package/dist/react-native/components/snap-separator.js +23 -0
  62. package/dist/react-native/components/snap-slider.d.ts +2 -0
  63. package/dist/react-native/components/snap-slider.js +43 -0
  64. package/dist/react-native/components/snap-stack.d.ts +5 -0
  65. package/dist/react-native/components/snap-stack.js +49 -0
  66. package/dist/react-native/components/snap-switch.d.ts +2 -0
  67. package/dist/react-native/components/snap-switch.js +31 -0
  68. package/dist/react-native/components/snap-text.d.ts +2 -0
  69. package/dist/react-native/components/snap-text.js +35 -0
  70. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  71. package/dist/react-native/components/snap-toggle-group.js +99 -0
  72. package/dist/react-native/confetti-overlay.d.ts +1 -0
  73. package/dist/react-native/confetti-overlay.js +106 -0
  74. package/dist/react-native/index.d.ts +28 -0
  75. package/dist/react-native/index.js +15 -0
  76. package/dist/react-native/snap-view-core.d.ts +11 -0
  77. package/dist/react-native/snap-view-core.js +153 -0
  78. package/dist/react-native/theme.d.ts +27 -0
  79. package/dist/react-native/theme.js +43 -0
  80. package/dist/react-native/types.d.ts +42 -0
  81. package/dist/react-native/types.js +1 -0
  82. package/dist/react-native/use-snap-palette.d.ts +13 -0
  83. package/dist/react-native/use-snap-palette.js +48 -0
  84. package/dist/react-native/v1/snap-view.d.ts +24 -0
  85. package/dist/react-native/v1/snap-view.js +96 -0
  86. package/dist/react-native/v2/snap-view.d.ts +33 -0
  87. package/dist/react-native/v2/snap-view.js +114 -0
  88. package/dist/schemas.d.ts +100 -13
  89. package/dist/schemas.js +28 -10
  90. package/dist/server/parseRequest.d.ts +10 -0
  91. package/dist/server/parseRequest.js +48 -7
  92. package/dist/server/verify.d.ts +1 -0
  93. package/dist/server/verify.js +1 -0
  94. package/dist/ui/badge.d.ts +7 -2
  95. package/dist/ui/badge.js +2 -0
  96. package/dist/ui/bar-chart.d.ts +30 -0
  97. package/dist/ui/bar-chart.js +30 -0
  98. package/dist/ui/button.d.ts +4 -6
  99. package/dist/ui/button.js +1 -1
  100. package/dist/ui/catalog.d.ts +90 -16
  101. package/dist/ui/catalog.js +17 -3
  102. package/dist/ui/cell-grid.d.ts +34 -0
  103. package/dist/ui/cell-grid.js +39 -0
  104. package/dist/ui/icon.d.ts +2 -2
  105. package/dist/ui/image.d.ts +1 -2
  106. package/dist/ui/image.js +1 -1
  107. package/dist/ui/index.d.ts +4 -0
  108. package/dist/ui/index.js +2 -0
  109. package/dist/ui/item.d.ts +1 -3
  110. package/dist/ui/item.js +1 -1
  111. package/dist/ui/schema.d.ts +6 -2
  112. package/dist/ui/schema.js +2 -2
  113. package/dist/ui/slider.d.ts +1 -0
  114. package/dist/ui/slider.js +2 -0
  115. package/dist/ui/text.d.ts +2 -4
  116. package/dist/ui/text.js +2 -2
  117. package/dist/validator.d.ts +3 -2
  118. package/dist/validator.js +198 -2
  119. package/llms.txt +199 -0
  120. package/package.json +9 -3
  121. package/src/colors.ts +20 -20
  122. package/src/constants.ts +23 -1
  123. package/src/index.ts +16 -13
  124. package/src/react/accent-context.tsx +13 -6
  125. package/src/react/catalog-renderer.tsx +4 -0
  126. package/src/react/components/action-button.tsx +47 -20
  127. package/src/react/components/badge.tsx +14 -18
  128. package/src/react/components/bar-chart.tsx +69 -0
  129. package/src/react/components/cell-grid.tsx +128 -0
  130. package/src/react/components/icon.tsx +5 -18
  131. package/src/react/components/input.tsx +20 -9
  132. package/src/react/components/item-group.tsx +4 -1
  133. package/src/react/components/item.tsx +13 -10
  134. package/src/react/components/progress.tsx +12 -7
  135. package/src/react/components/separator.tsx +8 -1
  136. package/src/react/components/slider.tsx +28 -15
  137. package/src/react/components/switch.tsx +12 -16
  138. package/src/react/components/text.tsx +14 -23
  139. package/src/react/components/toggle-group.tsx +26 -9
  140. package/src/react/hooks/use-snap-colors.ts +128 -0
  141. package/src/react/index.tsx +49 -265
  142. package/src/react/snap-view-core.tsx +340 -0
  143. package/src/react/v1/snap-view.tsx +176 -0
  144. package/src/react/v2/snap-view.tsx +199 -0
  145. package/src/react-native/catalog-renderer.tsx +41 -0
  146. package/src/react-native/components/snap-action-button.tsx +96 -0
  147. package/src/react-native/components/snap-badge.tsx +60 -0
  148. package/src/react-native/components/snap-bar-chart.tsx +73 -0
  149. package/src/react-native/components/snap-cell-grid.tsx +150 -0
  150. package/src/react-native/components/snap-icon.tsx +102 -0
  151. package/src/react-native/components/snap-image.tsx +37 -0
  152. package/src/react-native/components/snap-input.tsx +58 -0
  153. package/src/react-native/components/snap-item-group.tsx +43 -0
  154. package/src/react-native/components/snap-item.tsx +66 -0
  155. package/src/react-native/components/snap-progress.tsx +40 -0
  156. package/src/react-native/components/snap-separator.tsx +32 -0
  157. package/src/react-native/components/snap-slider.tsx +85 -0
  158. package/src/react-native/components/snap-stack.tsx +66 -0
  159. package/src/react-native/components/snap-switch.tsx +46 -0
  160. package/src/react-native/components/snap-text.tsx +51 -0
  161. package/src/react-native/components/snap-toggle-group.tsx +127 -0
  162. package/src/react-native/confetti-overlay.tsx +134 -0
  163. package/src/react-native/index.tsx +83 -0
  164. package/src/react-native/snap-view-core.tsx +209 -0
  165. package/src/react-native/theme.tsx +85 -0
  166. package/src/react-native/types.ts +38 -0
  167. package/src/react-native/use-snap-palette.ts +64 -0
  168. package/src/react-native/v1/snap-view.tsx +229 -0
  169. package/src/react-native/v2/snap-view.tsx +283 -0
  170. package/src/schemas.ts +68 -17
  171. package/src/server/parseRequest.ts +68 -9
  172. package/src/server/verify.ts +2 -0
  173. package/src/ui/README.md +8 -8
  174. package/src/ui/badge.ts +2 -0
  175. package/src/ui/bar-chart.ts +38 -0
  176. package/src/ui/button.ts +1 -1
  177. package/src/ui/catalog.ts +19 -3
  178. package/src/ui/cell-grid.ts +49 -0
  179. package/src/ui/image.ts +1 -1
  180. package/src/ui/index.ts +6 -0
  181. package/src/ui/item.ts +1 -1
  182. package/src/ui/schema.ts +2 -2
  183. package/src/ui/slider.ts +2 -0
  184. package/src/ui/text.ts +2 -2
  185. package/src/validator.ts +246 -2
  186. package/dist/dataStore.d.ts +0 -12
  187. package/dist/dataStore.js +0 -35
  188. package/dist/middleware.d.ts +0 -3
  189. package/dist/middleware.js +0 -3
  190. package/dist/react/hooks/use-snap-accent.d.ts +0 -13
  191. package/dist/react/hooks/use-snap-accent.js +0 -32
  192. package/src/dataStore.ts +0 -62
  193. package/src/middleware.ts +0 -7
  194. package/src/react/hooks/use-snap-accent.ts +0 -45
@@ -0,0 +1,41 @@
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
+ import { SnapBarChart } from "./components/snap-bar-chart";
18
+ import { SnapCellGrid } from "./components/snap-cell-grid";
19
+
20
+ /**
21
+ * Maps snap json-render catalog types to React Native primitives.
22
+ * Keys match the snap wire-format `type` strings exactly (snake_case).
23
+ */
24
+ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
25
+ badge: SnapBadge,
26
+ button: SnapActionButton,
27
+ icon: SnapIcon,
28
+ image: SnapImage,
29
+ input: SnapInput,
30
+ item: SnapItem,
31
+ item_group: SnapItemGroup,
32
+ progress: SnapProgress,
33
+ separator: SnapSeparator,
34
+ slider: SnapSlider,
35
+ stack: SnapStack,
36
+ switch: SnapSwitch,
37
+ text: SnapText,
38
+ toggle_group: SnapToggleGroup,
39
+ bar_chart: SnapBarChart,
40
+ cell_grid: SnapCellGrid,
41
+ });
@@ -0,0 +1,96 @@
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 { ExternalLink } from "lucide-react-native";
6
+ import { useSnapPalette } from "../use-snap-palette";
7
+ import { useSnapTheme } from "../theme";
8
+ import { ICON_MAP } from "./snap-icon";
9
+
10
+ function isExternalLinkAction(
11
+ on: Record<string, unknown> | undefined,
12
+ ): boolean {
13
+ if (!on) return false;
14
+ const press = on.press as
15
+ | { action?: string; params?: Record<string, unknown> }
16
+ | undefined;
17
+ if (!press) return false;
18
+ return press.action === "open_url";
19
+ }
20
+
21
+ export function SnapActionButton({
22
+ element,
23
+ emit,
24
+ }: ComponentRenderProps<Record<string, unknown>>) {
25
+ const { accentHex } = useSnapPalette();
26
+ const { colors } = useSnapTheme();
27
+ const { props } = element;
28
+ const label = String(props.label ?? "Action");
29
+ const variant = String(props.variant ?? "secondary");
30
+ const isPrimary = variant === "primary";
31
+ const iconName = props.icon ? String(props.icon) : undefined;
32
+
33
+ const textColor = isPrimary ? "#fff" : colors.text;
34
+ const iconColor = isPrimary ? "#fff" : colors.text;
35
+
36
+ const on = (element as unknown as { on?: Record<string, unknown> }).on;
37
+ const showExternalIcon = isExternalLinkAction(on);
38
+
39
+ return (
40
+ <View style={styles.outer}>
41
+ <Pressable
42
+ style={({ pressed }) => [
43
+ styles.btn,
44
+ isPrimary ? styles.btnDefault : styles.btnOther,
45
+ isPrimary
46
+ ? { backgroundColor: pressed ? accentHex + "DD" : accentHex }
47
+ : { backgroundColor: pressed ? colors.mutedHover : colors.muted },
48
+ pressed && styles.pressed,
49
+ ]}
50
+ onPress={() => {
51
+ void (async () => {
52
+ try {
53
+ await emit("press");
54
+ } catch (err: unknown) {
55
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
56
+ console.error("[snap] action failed", err);
57
+ }
58
+ }
59
+ })();
60
+ }}
61
+ >
62
+ {iconName && ICON_MAP[iconName]
63
+ ? (() => {
64
+ const I = ICON_MAP[iconName]!;
65
+ return <I size={16} color={iconColor} />;
66
+ })()
67
+ : null}
68
+ <Text style={{ color: textColor, fontSize: 14, lineHeight: 18, fontWeight: "600" }}>
69
+ {label}
70
+ </Text>
71
+ {showExternalIcon ? (
72
+ <ExternalLink size={14} color={iconColor} style={{ opacity: 0.6 }} />
73
+ ) : null}
74
+ </Pressable>
75
+ </View>
76
+ );
77
+ }
78
+
79
+ const styles = StyleSheet.create({
80
+ outer: { minWidth: 0 },
81
+ btn: {
82
+ paddingHorizontal: 16,
83
+ borderRadius: 10,
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ flexDirection: "row",
87
+ gap: 8,
88
+ },
89
+ btnDefault: {
90
+ paddingVertical: 10,
91
+ },
92
+ btnOther: {
93
+ paddingVertical: 8,
94
+ },
95
+ pressed: { opacity: 0.88 },
96
+ });
@@ -0,0 +1,60 @@
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 variant = String(props.variant ?? "default");
12
+ const color = props.color ? String(props.color) : undefined;
13
+ const iconName = props.icon ? String(props.icon) : undefined;
14
+ const isAccent = !color || color === "accent";
15
+ const resolvedColor = isAccent ? accentHex : hex(color);
16
+ const isFilled = variant !== "outline";
17
+
18
+ const Icon = iconName ? ICON_MAP[iconName] : undefined;
19
+
20
+ return (
21
+ <View
22
+ style={[
23
+ styles.badge,
24
+ isFilled
25
+ ? { backgroundColor: resolvedColor + "20", borderColor: "transparent" }
26
+ : { borderColor: resolvedColor },
27
+ ]}
28
+ >
29
+ {Icon && (
30
+ <Icon size={12} color={resolvedColor} />
31
+ )}
32
+ <Text
33
+ style={[
34
+ styles.label,
35
+ { color: resolvedColor },
36
+ ]}
37
+ >
38
+ {label}
39
+ </Text>
40
+ </View>
41
+ );
42
+ }
43
+
44
+ const styles = StyleSheet.create({
45
+ badge: {
46
+ alignSelf: "flex-start",
47
+ flexDirection: "row",
48
+ alignItems: "center",
49
+ gap: 4,
50
+ paddingHorizontal: 8,
51
+ paddingVertical: 2,
52
+ borderRadius: 9999,
53
+ borderWidth: 1,
54
+ },
55
+ label: {
56
+ fontSize: 12,
57
+ lineHeight: 16,
58
+ fontWeight: "500",
59
+ },
60
+ });
@@ -0,0 +1,73 @@
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 SnapBarChart({
7
+ element: { props },
8
+ }: ComponentRenderProps<Record<string, unknown>>) {
9
+ const { accentHex, hex } = useSnapPalette();
10
+ const { colors } = useSnapTheme();
11
+ const bars = Array.isArray(props.bars) ? props.bars : [];
12
+ const chartColor = String(props.color ?? "accent");
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 hex(bar.color);
23
+ if (chartColor !== "accent") return hex(chartColor);
24
+ return accentHex;
25
+ }
26
+
27
+ return (
28
+ <View style={styles.wrap}>
29
+ {bars.map(
30
+ (
31
+ bar: { label?: string; value?: number; color?: string },
32
+ i: number,
33
+ ) => {
34
+ const value = Number(bar.value ?? 0);
35
+ const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
36
+ return (
37
+ <View key={i} style={styles.row}>
38
+ <Text
39
+ style={[styles.label, { color: colors.textSecondary }]}
40
+ numberOfLines={1}
41
+ >
42
+ {String(bar.label ?? "")}
43
+ </Text>
44
+ <View style={[styles.track, { backgroundColor: colors.muted }]}>
45
+ <View
46
+ style={[
47
+ styles.fill,
48
+ {
49
+ width: `${pct}%`,
50
+ backgroundColor: barFill(bar),
51
+ },
52
+ ]}
53
+ />
54
+ </View>
55
+ <Text style={[styles.value, { color: colors.textSecondary }]}>
56
+ {value}
57
+ </Text>
58
+ </View>
59
+ );
60
+ },
61
+ )}
62
+ </View>
63
+ );
64
+ }
65
+
66
+ const styles = StyleSheet.create({
67
+ wrap: { width: "100%", gap: 8 },
68
+ row: { flexDirection: "row", alignItems: "center", gap: 8 },
69
+ label: { width: 80, fontSize: 12, lineHeight: 16, textAlign: "right" },
70
+ track: { flex: 1, height: 10, borderRadius: 9999, overflow: "hidden" },
71
+ fill: { height: "100%", borderRadius: 9999 },
72
+ value: { width: 32, fontSize: 12, lineHeight: 16, fontVariant: ["tabular-nums"] },
73
+ });
@@ -0,0 +1,150 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { StyleSheet, Text, View, Pressable } from "react-native";
3
+ import { useStateStore } from "@json-render/react-native";
4
+ import { useSnapPalette } from "../use-snap-palette";
5
+ import { useSnapTheme } from "../theme";
6
+ import { POST_GRID_TAP_KEY } from "@farcaster/snap";
7
+
8
+ export function SnapCellGrid({
9
+ element: { props },
10
+ }: ComponentRenderProps<Record<string, unknown>>) {
11
+ const { hex, appearance } = useSnapPalette();
12
+ const { colors } = useSnapTheme();
13
+ const { get, set } = useStateStore();
14
+ const cols = Number(props.cols ?? 2);
15
+ const rows = Number(props.rows ?? 2);
16
+ const cells = Array.isArray(props.cells) ? props.cells : [];
17
+ const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
18
+ const gap = String(props.gap ?? "sm");
19
+ const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
20
+ const gapPx = gapMap[gap] ?? 1;
21
+
22
+ const select = String(props.select ?? "off");
23
+ const interactive = select !== "off";
24
+ const isMultiple = select === "multiple";
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
+ const selectedSet = new Set<string>();
31
+ if (typeof tapRaw === "string" && tapRaw.length > 0) {
32
+ for (const part of tapRaw.split("|")) {
33
+ if (part.includes(",")) selectedSet.add(part);
34
+ }
35
+ }
36
+
37
+ const isSelected = (r: number, c: number) => selectedSet.has(`${r},${c}`);
38
+
39
+ const handleTap = (r: number, c: number) => {
40
+ const key = `${r},${c}`;
41
+ if (isMultiple) {
42
+ const next = new Set(selectedSet);
43
+ if (next.has(key)) next.delete(key);
44
+ else next.add(key);
45
+ set(tapPath, [...next].join("|"));
46
+ } else {
47
+ set(tapPath, key);
48
+ }
49
+ };
50
+
51
+ const cellMap = new Map<string, { color?: string; content?: string }>();
52
+ for (const c of cells) {
53
+ cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
54
+ color: c.color as string | undefined,
55
+ content: c.content != null ? String(c.content) : undefined,
56
+ });
57
+ }
58
+
59
+ const ringOuter = appearance === "dark" ? "#fff" : "#000";
60
+ const ringInner = appearance === "dark" ? "#000" : "#fff";
61
+
62
+ const rowEls = [];
63
+ for (let r = 0; r < rows; r++) {
64
+ const rowCells = [];
65
+ for (let c = 0; c < cols; c++) {
66
+ const cell = cellMap.get(`${r},${c}`);
67
+ const selected = interactive && isSelected(r, c);
68
+ const bg = cell?.color ? hex(cell.color) : "transparent";
69
+
70
+ const cellContent = cell?.content ? (
71
+ <Text style={[styles.cellText, { color: colors.textPrimary }]}>
72
+ {cell.content}
73
+ </Text>
74
+ ) : null;
75
+
76
+ // Two-tone ring: outer View with contrasting border, inner View with inverse border
77
+ const cellView = selected ? (
78
+ <View style={[styles.cell, { height: rowHeight, borderWidth: 1, borderColor: ringOuter, borderRadius: 4 }]}>
79
+ <View
80
+ style={[
81
+ styles.innerCell,
82
+ { backgroundColor: bg, borderWidth: 1, borderColor: ringInner, borderRadius: 3 },
83
+ ]}
84
+ >
85
+ {cellContent}
86
+ </View>
87
+ </View>
88
+ ) : (
89
+ <View style={[styles.cell, { height: rowHeight, backgroundColor: bg }]}>
90
+ {cellContent}
91
+ </View>
92
+ );
93
+
94
+ rowCells.push(
95
+ interactive ? (
96
+ <Pressable
97
+ key={`${r}-${c}`}
98
+ onPress={() => handleTap(r, c)}
99
+ style={styles.cellWrap}
100
+ >
101
+ {cellView}
102
+ </Pressable>
103
+ ) : (
104
+ <View key={`${r}-${c}`} style={styles.cellWrap}>
105
+ {cellView}
106
+ </View>
107
+ ),
108
+ );
109
+ }
110
+ rowEls.push(
111
+ <View key={r} style={[styles.gridRow, { gap: gapPx }]}>
112
+ {rowCells}
113
+ </View>,
114
+ );
115
+ }
116
+
117
+ const selectionLabel = interactive && selectedSet.size > 0
118
+ ? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
119
+ : null;
120
+
121
+ return (
122
+ <View style={[styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }]}>
123
+ {rowEls}
124
+ {selectionLabel ? (
125
+ <Text style={[styles.selectionText, { color: colors.textSecondary }]}>
126
+ {selectionLabel}
127
+ </Text>
128
+ ) : null}
129
+ </View>
130
+ );
131
+ }
132
+
133
+ const styles = StyleSheet.create({
134
+ wrap: { width: "100%" },
135
+ gridRow: { flexDirection: "row" },
136
+ cellWrap: { flex: 1 },
137
+ cell: {
138
+ borderRadius: 4,
139
+ alignItems: "center",
140
+ justifyContent: "center",
141
+ },
142
+ innerCell: {
143
+ width: "100%",
144
+ height: "100%",
145
+ alignItems: "center",
146
+ justifyContent: "center",
147
+ },
148
+ cellText: { fontSize: 12, lineHeight: 16, fontWeight: "600" },
149
+ selectionText: { fontSize: 11, fontFamily: "monospace", marginTop: 6 },
150
+ });
@@ -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,37 @@
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
+ width: "100%",
33
+ borderRadius: 8,
34
+ overflow: "hidden",
35
+ backgroundColor: "#f3f4f6",
36
+ },
37
+ });
@@ -0,0 +1,58 @@
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, lineHeight: 18, fontWeight: "500" },
50
+ input: {
51
+ borderWidth: 1,
52
+ borderRadius: 8,
53
+ paddingHorizontal: 12,
54
+ paddingVertical: 10,
55
+ fontSize: 14,
56
+ lineHeight: 18,
57
+ },
58
+ });
@@ -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
+ });