@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,66 @@
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 = { paddingVertical: 6, paddingHorizontal: 10 };
18
+
19
+ return (
20
+ <View style={[styles.container, containerVariant]}>
21
+ <View style={styles.content}>
22
+ {title ? <Text style={[styles.title, { color: colors.text }]}>{title}</Text> : null}
23
+ {description ? (
24
+ <Text style={[styles.description, { color: colors.textSecondary }]}>
25
+ {description}
26
+ </Text>
27
+ ) : null}
28
+ </View>
29
+ {children ? (
30
+ <View style={styles.actions}>
31
+ <View style={{ flex: 0 }}>{children}</View>
32
+ </View>
33
+ ) : null}
34
+ </View>
35
+ );
36
+ }
37
+
38
+ const styles = StyleSheet.create({
39
+ container: {
40
+ flexDirection: "row",
41
+ alignItems: "center",
42
+ },
43
+ content: {
44
+ flex: 1,
45
+ },
46
+ title: {
47
+ fontSize: 15,
48
+ lineHeight: 20,
49
+ fontWeight: "500",
50
+ },
51
+ description: {
52
+ fontSize: 13,
53
+ lineHeight: 18,
54
+ marginTop: 1,
55
+ },
56
+ actions: {
57
+ marginLeft: "auto",
58
+ paddingLeft: 12,
59
+ flexDirection: "row",
60
+ alignItems: "center",
61
+ flexShrink: 0,
62
+ flexGrow: 0,
63
+ flexBasis: "auto",
64
+ gap: 4,
65
+ },
66
+ });
@@ -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: { width: "100%", gap: 4 },
30
+ label: { fontSize: 13, lineHeight: 18 },
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,85 @@
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 showValue = props.showValue === true;
30
+ const minLabel = props.minLabel != null ? String(props.minLabel) : null;
31
+ const maxLabel = props.maxLabel != null ? String(props.maxLabel) : null;
32
+
33
+ return (
34
+ <View style={styles.wrap}>
35
+ {label ? (
36
+ <View style={styles.labelRow}>
37
+ <Text style={[styles.label, { color: colors.text }]}>{label}</Text>
38
+ {showValue && (
39
+ <Text style={[styles.valueText, { color: colors.textSecondary }]}>
40
+ {String(Math.round(clamped))}
41
+ </Text>
42
+ )}
43
+ </View>
44
+ ) : null}
45
+ <Slider
46
+ style={styles.slider}
47
+ minimumValue={min}
48
+ maximumValue={max}
49
+ step={step > 0 ? step : 1}
50
+ value={clamped}
51
+ onValueChange={(v) => set(path, v)}
52
+ minimumTrackTintColor={accentHex}
53
+ maximumTrackTintColor={colors.muted}
54
+ thumbTintColor={accentHex}
55
+ />
56
+ {minLabel != null || maxLabel != null ? (
57
+ <View style={styles.minMaxRow}>
58
+ <Text style={[styles.minMax, { color: colors.textSecondary }]}>
59
+ {minLabel ?? String(min)}
60
+ </Text>
61
+ <Text style={[styles.minMax, { color: colors.textSecondary }]}>
62
+ {maxLabel ?? String(max)}
63
+ </Text>
64
+ </View>
65
+ ) : null}
66
+ </View>
67
+ );
68
+ }
69
+
70
+ const styles = StyleSheet.create({
71
+ wrap: { width: "100%", gap: 2 },
72
+ labelRow: {
73
+ flexDirection: "row",
74
+ justifyContent: "space-between",
75
+ alignItems: "center",
76
+ },
77
+ label: { fontSize: 13, lineHeight: 18, fontWeight: "500", flex: 1 },
78
+ valueText: { fontSize: 13, lineHeight: 18 },
79
+ slider: { width: "100%", height: 40 },
80
+ minMaxRow: {
81
+ flexDirection: "row",
82
+ justifyContent: "space-between",
83
+ },
84
+ minMax: { fontSize: 12, lineHeight: 16 },
85
+ });
@@ -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,46 @@
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.muted, 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
+ lineHeight: 18,
43
+ fontWeight: "400",
44
+ flex: 1,
45
+ },
46
+ });
@@ -0,0 +1,51 @@
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
+ md: { fontSize: 16, lineHeight: 24 },
7
+ sm: { fontSize: 13, lineHeight: 18 },
8
+ };
9
+
10
+ const WEIGHT_MAP: Record<string, "400" | "500" | "600" | "700"> = {
11
+ bold: "700",
12
+ normal: "400",
13
+ };
14
+
15
+ export function SnapText({
16
+ element: { props },
17
+ }: ComponentRenderProps<Record<string, unknown>>) {
18
+ const { colors } = useSnapTheme();
19
+ const content = String(props.content ?? "");
20
+ const size = String(props.size ?? "md");
21
+ const weight = props.weight ? String(props.weight) : undefined;
22
+ const align = (props.align as "left" | "center" | "right" | undefined) ?? undefined;
23
+
24
+ const sizeStyle = SIZE_STYLES[size] ?? SIZE_STYLES.md;
25
+ const resolvedWeight = weight ? WEIGHT_MAP[weight] : sizeStyle?.fontWeight;
26
+ const textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
27
+
28
+ return (
29
+ <View style={styles.wrap}>
30
+ <Text
31
+ style={[
32
+ styles.base,
33
+ {
34
+ color: colors.text,
35
+ fontSize: sizeStyle!.fontSize,
36
+ lineHeight: sizeStyle!.lineHeight,
37
+ fontWeight: resolvedWeight,
38
+ textAlign,
39
+ },
40
+ ]}
41
+ >
42
+ {content}
43
+ </Text>
44
+ </View>
45
+ );
46
+ }
47
+
48
+ const styles = StyleSheet.create({
49
+ wrap: { width: "100%" },
50
+ base: {},
51
+ });
@@ -0,0 +1,127 @@
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 { useSnapTheme } from "../theme";
5
+
6
+ export function SnapToggleGroup({
7
+ element: { props },
8
+ }: ComponentRenderProps<Record<string, unknown>>) {
9
+ const { get, set } = useStateStore();
10
+ const { colors } = useSnapTheme();
11
+ const name = String(props.name ?? "toggle_group");
12
+ const path = `/inputs/${name}`;
13
+ const label = props.label ? String(props.label) : undefined;
14
+ const isMultiple = Boolean(props.multiple);
15
+ const orientation = String(props.orientation ?? "horizontal");
16
+ const options = Array.isArray(props.options)
17
+ ? (props.options as string[])
18
+ : [];
19
+
20
+ const raw = get(path);
21
+ const defaultValue = props.defaultValue;
22
+
23
+ const selected = (() => {
24
+ if (raw !== undefined && raw !== null) {
25
+ return isMultiple
26
+ ? Array.isArray(raw)
27
+ ? (raw as string[])
28
+ : []
29
+ : typeof raw === "string"
30
+ ? [raw]
31
+ : [];
32
+ }
33
+ if (defaultValue !== undefined) {
34
+ return Array.isArray(defaultValue)
35
+ ? (defaultValue as string[])
36
+ : [String(defaultValue)];
37
+ }
38
+ return [];
39
+ })();
40
+
41
+ const isVertical = orientation === "vertical";
42
+
43
+ const handlePress = (opt: string) => {
44
+ if (isMultiple) {
45
+ const next = selected.includes(opt)
46
+ ? selected.filter((s) => s !== opt)
47
+ : [...selected, opt];
48
+ set(path, next);
49
+ } else {
50
+ if (opt && opt !== selected[0]) set(path, opt);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <View style={styles.wrap}>
56
+ {label ? <Text style={[styles.label, { color: colors.text }]}>{label}</Text> : null}
57
+ <View
58
+ style={[
59
+ styles.group,
60
+ { backgroundColor: colors.muted },
61
+ isVertical ? styles.groupVertical : styles.groupHorizontal,
62
+ ]}
63
+ >
64
+ {options.map((opt, index) => {
65
+ const isSelected = selected.includes(opt);
66
+ return (
67
+ <Pressable
68
+ key={index}
69
+ style={({ pressed }) => [
70
+ styles.option,
71
+ {
72
+ backgroundColor: isSelected
73
+ ? colors.mutedSelected
74
+ : pressed
75
+ ? colors.mutedHover
76
+ : colors.mutedSubtle,
77
+ },
78
+ !isVertical && styles.optionHorizontal,
79
+ ]}
80
+ onPress={() => handlePress(opt)}
81
+ >
82
+ <Text
83
+ style={[
84
+ styles.optionText,
85
+ { color: colors.text },
86
+ ]}
87
+ >
88
+ {opt}
89
+ </Text>
90
+ </Pressable>
91
+ );
92
+ })}
93
+ </View>
94
+ </View>
95
+ );
96
+ }
97
+
98
+ const styles = StyleSheet.create({
99
+ wrap: { width: "100%", gap: 6 },
100
+ label: { fontSize: 13, lineHeight: 18, fontWeight: "500" },
101
+ group: {
102
+ padding: 4,
103
+ borderRadius: 8,
104
+ gap: 4,
105
+ },
106
+ groupHorizontal: {
107
+ flexDirection: "row",
108
+ },
109
+ groupVertical: {
110
+ flexDirection: "column",
111
+ },
112
+ option: {
113
+ paddingVertical: 8,
114
+ paddingHorizontal: 12,
115
+ borderRadius: 6,
116
+ alignItems: "center",
117
+ justifyContent: "center",
118
+ },
119
+ optionHorizontal: {
120
+ flex: 1,
121
+ },
122
+ optionText: {
123
+ fontSize: 13,
124
+ lineHeight: 18,
125
+ fontWeight: "500",
126
+ },
127
+ });
@@ -0,0 +1,134 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import {
3
+ Animated,
4
+ StyleSheet,
5
+ View,
6
+ useWindowDimensions,
7
+ } from "react-native";
8
+
9
+ const CONFETTI_COLORS = [
10
+ "#907AA9",
11
+ "#EC4899",
12
+ "#3B82F6",
13
+ "#10B981",
14
+ "#F59E0B",
15
+ "#EF4444",
16
+ "#06B6D4",
17
+ ];
18
+
19
+ export function ConfettiOverlay() {
20
+ const { width, height } = useWindowDimensions();
21
+
22
+ const pieces = useMemo(
23
+ () =>
24
+ Array.from({ length: 80 }, (_, i) => ({
25
+ id: i,
26
+ left: Math.random() * width,
27
+ delay: Math.random() * 1200,
28
+ duration: 2500 + Math.random() * 2000,
29
+ color:
30
+ CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)]!,
31
+ size: 6 + Math.random() * 8,
32
+ startRotation: Math.random() * 360,
33
+ driftX: (Math.random() > 0.5 ? 1 : -1) * Math.random() * 40,
34
+ })),
35
+ // width captured once on mount; intentional stable dep
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ [],
38
+ );
39
+
40
+ const anims = useRef(
41
+ pieces.map(() => ({
42
+ translateY: new Animated.Value(-20),
43
+ opacity: new Animated.Value(1),
44
+ rotate: new Animated.Value(0),
45
+ translateX: new Animated.Value(0),
46
+ })),
47
+ ).current;
48
+
49
+ useEffect(() => {
50
+ const animations = pieces.map((piece, i) => {
51
+ const anim = anims[i]!;
52
+ anim.translateY.setValue(-20);
53
+ anim.opacity.setValue(1);
54
+ anim.rotate.setValue(0);
55
+ anim.translateX.setValue(0);
56
+
57
+ return Animated.sequence([
58
+ Animated.delay(piece.delay),
59
+ Animated.parallel([
60
+ Animated.timing(anim.translateY, {
61
+ toValue: height + 20,
62
+ duration: piece.duration,
63
+ useNativeDriver: true,
64
+ }),
65
+ Animated.timing(anim.opacity, {
66
+ toValue: 0,
67
+ duration: piece.duration,
68
+ useNativeDriver: true,
69
+ }),
70
+ Animated.timing(anim.rotate, {
71
+ toValue: 720,
72
+ duration: piece.duration,
73
+ useNativeDriver: true,
74
+ }),
75
+ Animated.timing(anim.translateX, {
76
+ toValue: piece.driftX,
77
+ duration: piece.duration,
78
+ useNativeDriver: true,
79
+ }),
80
+ ]),
81
+ ]);
82
+ });
83
+
84
+ const composite = Animated.parallel(animations);
85
+ composite.start();
86
+ return () => composite.stop();
87
+ }, [pieces, anims, height]);
88
+
89
+ return (
90
+ <View style={[StyleSheet.absoluteFill, styles.container]} pointerEvents="none">
91
+ {pieces.map((piece, i) => {
92
+ const anim = anims[i]!;
93
+ const rotate = anim.rotate.interpolate({
94
+ inputRange: [0, 720],
95
+ outputRange: [
96
+ `${piece.startRotation}deg`,
97
+ `${piece.startRotation + 720}deg`,
98
+ ],
99
+ });
100
+ return (
101
+ <Animated.View
102
+ key={piece.id}
103
+ style={[
104
+ styles.piece,
105
+ {
106
+ left: piece.left,
107
+ width: piece.size,
108
+ height: piece.size * 0.6,
109
+ backgroundColor: piece.color,
110
+ opacity: anim.opacity,
111
+ transform: [
112
+ { translateY: anim.translateY },
113
+ { translateX: anim.translateX },
114
+ { rotate },
115
+ ],
116
+ },
117
+ ]}
118
+ />
119
+ );
120
+ })}
121
+ </View>
122
+ );
123
+ }
124
+
125
+ const styles = StyleSheet.create({
126
+ container: {
127
+ overflow: "hidden",
128
+ },
129
+ piece: {
130
+ position: "absolute",
131
+ top: 0,
132
+ borderRadius: 2,
133
+ },
134
+ });