@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,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
+ });
@@ -0,0 +1,82 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { useStateStore } from "@json-render/react-native";
3
+ import Slider from "@react-native-community/slider";
4
+ import { StyleSheet, Text, View } from "react-native";
5
+ import { useSnapPalette } from "../use-snap-palette";
6
+ import { useSnapTheme } from "../theme";
7
+
8
+ export function SnapSlider({
9
+ element: { props },
10
+ }: ComponentRenderProps<Record<string, unknown>>) {
11
+ const { get, set } = useStateStore();
12
+ const { accentHex } = useSnapPalette();
13
+ const { colors } = useSnapTheme();
14
+ const name = String(props.name ?? "slider");
15
+ const path = `/inputs/${name}`;
16
+ const min = Number(props.min ?? 0);
17
+ const max = Number(props.max ?? 100);
18
+ const step = props.step != null ? Number(props.step) : 1;
19
+ const fallback =
20
+ props.defaultValue != null ? Number(props.defaultValue) : (min + max) / 2;
21
+ const raw = get(path);
22
+ const value =
23
+ raw === undefined || raw === null ? fallback : Number(raw);
24
+ const clamped = Number.isFinite(value)
25
+ ? Math.min(max, Math.max(min, value))
26
+ : fallback;
27
+
28
+ const label = props.label != null ? String(props.label) : null;
29
+ const minLabel = props.minLabel != null ? String(props.minLabel) : null;
30
+ const maxLabel = props.maxLabel != null ? String(props.maxLabel) : null;
31
+
32
+ return (
33
+ <View style={styles.wrap}>
34
+ {label ? (
35
+ <View style={styles.labelRow}>
36
+ <Text style={[styles.label, { color: colors.text }]}>{label}</Text>
37
+ <Text style={[styles.valueText, { color: colors.textSecondary }]}>
38
+ {String(Math.round(clamped))}
39
+ </Text>
40
+ </View>
41
+ ) : null}
42
+ <Slider
43
+ style={styles.slider}
44
+ minimumValue={min}
45
+ maximumValue={max}
46
+ step={step > 0 ? step : 1}
47
+ value={clamped}
48
+ onValueChange={(v) => set(path, v)}
49
+ minimumTrackTintColor={accentHex}
50
+ maximumTrackTintColor={colors.muted}
51
+ thumbTintColor={accentHex}
52
+ />
53
+ {minLabel != null || maxLabel != null ? (
54
+ <View style={styles.minMaxRow}>
55
+ <Text style={[styles.minMax, { color: colors.textSecondary }]}>
56
+ {minLabel ?? String(min)}
57
+ </Text>
58
+ <Text style={[styles.minMax, { color: colors.textSecondary }]}>
59
+ {maxLabel ?? String(max)}
60
+ </Text>
61
+ </View>
62
+ ) : null}
63
+ </View>
64
+ );
65
+ }
66
+
67
+ const styles = StyleSheet.create({
68
+ wrap: { width: "100%", gap: 6 },
69
+ labelRow: {
70
+ flexDirection: "row",
71
+ justifyContent: "space-between",
72
+ alignItems: "center",
73
+ },
74
+ label: { fontSize: 13, fontWeight: "500", flex: 1 },
75
+ valueText: { fontSize: 13 },
76
+ slider: { width: "100%", height: 40 },
77
+ minMaxRow: {
78
+ flexDirection: "row",
79
+ justifyContent: "space-between",
80
+ },
81
+ minMax: { fontSize: 12 },
82
+ });
@@ -0,0 +1,66 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import type { ReactNode } from "react";
3
+ import { StyleSheet, View } from "react-native";
4
+
5
+ const VGAP: Record<string, number> = {
6
+ none: 0,
7
+ sm: 8,
8
+ md: 16,
9
+ lg: 24,
10
+ };
11
+
12
+ const HGAP: Record<string, number> = {
13
+ none: 0,
14
+ sm: 4,
15
+ md: 8,
16
+ lg: 12,
17
+ };
18
+
19
+ const JUSTIFY: Record<string, "flex-start" | "center" | "flex-end" | "space-between" | "space-around"> = {
20
+ start: "flex-start",
21
+ center: "center",
22
+ end: "flex-end",
23
+ between: "space-between",
24
+ around: "space-around",
25
+ };
26
+
27
+ export function SnapStack({
28
+ element: { props },
29
+ children,
30
+ }: ComponentRenderProps<Record<string, unknown>> & { children?: ReactNode }) {
31
+ const direction = String(props.direction ?? "vertical");
32
+ const rawGap = props.gap;
33
+ const isHorizontal = direction === "horizontal";
34
+ const gapMap = isHorizontal ? HGAP : VGAP;
35
+ const gap =
36
+ typeof rawGap === "number"
37
+ ? rawGap
38
+ : typeof rawGap === "string" && rawGap in gapMap
39
+ ? gapMap[rawGap]!
40
+ : isHorizontal ? HGAP.md! : VGAP.md!;
41
+ const justify = props.justify ? JUSTIFY[String(props.justify)] : undefined;
42
+
43
+ return (
44
+ <View
45
+ style={[
46
+ styles.stack,
47
+ isHorizontal ? styles.horizontal : undefined,
48
+ { gap },
49
+ justify ? { justifyContent: justify } : undefined,
50
+ ]}
51
+ >
52
+ {children}
53
+ </View>
54
+ );
55
+ }
56
+
57
+ const styles = StyleSheet.create({
58
+ stack: {
59
+ width: "100%",
60
+ },
61
+ horizontal: {
62
+ flexDirection: "row",
63
+ alignItems: "center",
64
+ flexWrap: "wrap",
65
+ },
66
+ });
@@ -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
+ });