@farcaster/snap 2.0.0 → 2.0.2
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.
- package/dist/colors.d.ts +4 -4
- package/dist/colors.js +20 -20
- package/dist/constants.d.ts +17 -1
- package/dist/constants.js +19 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/react/accent-context.d.ts +3 -1
- package/dist/react/accent-context.js +7 -4
- package/dist/react/catalog-renderer.js +4 -0
- package/dist/react/components/action-button.d.ts +2 -1
- package/dist/react/components/action-button.js +35 -13
- package/dist/react/components/badge.js +8 -8
- package/dist/react/components/bar-chart.d.ts +5 -0
- package/dist/react/components/bar-chart.js +26 -0
- package/dist/react/components/cell-grid.d.ts +5 -0
- package/dist/react/components/cell-grid.js +87 -0
- package/dist/react/components/icon.js +4 -10
- package/dist/react/components/input.js +12 -6
- package/dist/react/components/item-group.js +3 -1
- package/dist/react/components/item.d.ts +3 -3
- package/dist/react/components/item.js +4 -3
- package/dist/react/components/progress.js +3 -3
- package/dist/react/components/separator.js +3 -1
- package/dist/react/components/slider.js +15 -10
- package/dist/react/components/switch.js +10 -12
- package/dist/react/components/text.js +6 -14
- package/dist/react/components/toggle-group.js +20 -6
- package/dist/react/hooks/use-snap-colors.d.ts +38 -0
- package/dist/react/hooks/use-snap-colors.js +81 -0
- package/dist/react/index.d.ts +13 -1
- package/dist/react/index.js +9 -188
- package/dist/react/snap-view-core.d.ts +11 -0
- package/dist/react/snap-view-core.js +227 -0
- package/dist/react/v1/snap-view.d.ts +16 -0
- package/dist/react/v1/snap-view.js +90 -0
- package/dist/react/v2/snap-view.d.ts +23 -0
- package/dist/react/v2/snap-view.js +91 -0
- package/dist/react-native/catalog-renderer.d.ts +5 -0
- package/dist/react-native/catalog-renderer.js +40 -0
- package/dist/react-native/components/snap-action-button.d.ts +2 -0
- package/dist/react-native/components/snap-action-button.js +69 -0
- package/dist/react-native/components/snap-badge.d.ts +2 -0
- package/dist/react-native/components/snap-badge.js +41 -0
- package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
- package/dist/react-native/components/snap-bar-chart.js +39 -0
- package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
- package/dist/react-native/components/snap-cell-grid.js +94 -0
- package/dist/react-native/components/snap-icon.d.ts +5 -0
- package/dist/react-native/components/snap-icon.js +56 -0
- package/dist/react-native/components/snap-image.d.ts +2 -0
- package/dist/react-native/components/snap-image.js +23 -0
- package/dist/react-native/components/snap-input.d.ts +2 -0
- package/dist/react-native/components/snap-input.js +37 -0
- package/dist/react-native/components/snap-item-group.d.ts +5 -0
- package/dist/react-native/components/snap-item-group.js +23 -0
- package/dist/react-native/components/snap-item.d.ts +5 -0
- package/dist/react-native/components/snap-item.js +42 -0
- package/dist/react-native/components/snap-progress.d.ts +2 -0
- package/dist/react-native/components/snap-progress.js +26 -0
- package/dist/react-native/components/snap-separator.d.ts +2 -0
- package/dist/react-native/components/snap-separator.js +23 -0
- package/dist/react-native/components/snap-slider.d.ts +2 -0
- package/dist/react-native/components/snap-slider.js +43 -0
- package/dist/react-native/components/snap-stack.d.ts +5 -0
- package/dist/react-native/components/snap-stack.js +49 -0
- package/dist/react-native/components/snap-switch.d.ts +2 -0
- package/dist/react-native/components/snap-switch.js +31 -0
- package/dist/react-native/components/snap-text.d.ts +2 -0
- package/dist/react-native/components/snap-text.js +35 -0
- package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
- package/dist/react-native/components/snap-toggle-group.js +99 -0
- package/dist/react-native/confetti-overlay.d.ts +1 -0
- package/dist/react-native/confetti-overlay.js +106 -0
- package/dist/react-native/index.d.ts +28 -0
- package/dist/react-native/index.js +15 -0
- package/dist/react-native/snap-view-core.d.ts +11 -0
- package/dist/react-native/snap-view-core.js +156 -0
- package/dist/react-native/theme.d.ts +27 -0
- package/dist/react-native/theme.js +43 -0
- package/dist/react-native/types.d.ts +42 -0
- package/dist/react-native/types.js +1 -0
- package/dist/react-native/use-snap-palette.d.ts +13 -0
- package/dist/react-native/use-snap-palette.js +48 -0
- package/dist/react-native/v1/snap-view.d.ts +24 -0
- package/dist/react-native/v1/snap-view.js +96 -0
- package/dist/react-native/v2/snap-view.d.ts +33 -0
- package/dist/react-native/v2/snap-view.js +114 -0
- package/dist/schemas.d.ts +100 -13
- package/dist/schemas.js +28 -10
- package/dist/server/parseRequest.d.ts +10 -0
- package/dist/server/parseRequest.js +48 -7
- package/dist/server/verify.d.ts +1 -0
- package/dist/server/verify.js +1 -0
- package/dist/ui/badge.d.ts +7 -2
- package/dist/ui/badge.js +2 -0
- package/dist/ui/bar-chart.d.ts +30 -0
- package/dist/ui/bar-chart.js +30 -0
- package/dist/ui/button.d.ts +4 -6
- package/dist/ui/button.js +1 -1
- package/dist/ui/catalog.d.ts +90 -16
- package/dist/ui/catalog.js +17 -3
- package/dist/ui/cell-grid.d.ts +34 -0
- package/dist/ui/cell-grid.js +39 -0
- package/dist/ui/icon.d.ts +2 -2
- package/dist/ui/image.d.ts +1 -2
- package/dist/ui/image.js +1 -1
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/item.d.ts +1 -3
- package/dist/ui/item.js +1 -1
- package/dist/ui/schema.d.ts +6 -2
- package/dist/ui/schema.js +2 -2
- package/dist/ui/slider.d.ts +1 -0
- package/dist/ui/slider.js +2 -0
- package/dist/ui/text.d.ts +2 -4
- package/dist/ui/text.js +2 -2
- package/dist/validator.d.ts +3 -2
- package/dist/validator.js +203 -2
- package/llms.txt +199 -0
- package/package.json +9 -3
- package/src/colors.ts +20 -20
- package/src/constants.ts +23 -1
- package/src/index.ts +16 -13
- package/src/react/accent-context.tsx +13 -6
- package/src/react/catalog-renderer.tsx +4 -0
- package/src/react/components/action-button.tsx +50 -20
- package/src/react/components/badge.tsx +14 -18
- package/src/react/components/bar-chart.tsx +69 -0
- package/src/react/components/cell-grid.tsx +128 -0
- package/src/react/components/icon.tsx +5 -18
- package/src/react/components/input.tsx +20 -9
- package/src/react/components/item-group.tsx +4 -1
- package/src/react/components/item.tsx +13 -10
- package/src/react/components/progress.tsx +12 -7
- package/src/react/components/separator.tsx +8 -1
- package/src/react/components/slider.tsx +28 -15
- package/src/react/components/switch.tsx +12 -16
- package/src/react/components/text.tsx +14 -23
- package/src/react/components/toggle-group.tsx +26 -9
- package/src/react/hooks/use-snap-colors.ts +128 -0
- package/src/react/index.tsx +49 -265
- package/src/react/snap-view-core.tsx +343 -0
- package/src/react/v1/snap-view.tsx +176 -0
- package/src/react/v2/snap-view.tsx +199 -0
- package/src/react-native/catalog-renderer.tsx +41 -0
- package/src/react-native/components/snap-action-button.tsx +96 -0
- package/src/react-native/components/snap-badge.tsx +60 -0
- package/src/react-native/components/snap-bar-chart.tsx +73 -0
- package/src/react-native/components/snap-cell-grid.tsx +150 -0
- package/src/react-native/components/snap-icon.tsx +102 -0
- package/src/react-native/components/snap-image.tsx +37 -0
- package/src/react-native/components/snap-input.tsx +58 -0
- package/src/react-native/components/snap-item-group.tsx +43 -0
- package/src/react-native/components/snap-item.tsx +66 -0
- package/src/react-native/components/snap-progress.tsx +40 -0
- package/src/react-native/components/snap-separator.tsx +32 -0
- package/src/react-native/components/snap-slider.tsx +85 -0
- package/src/react-native/components/snap-stack.tsx +66 -0
- package/src/react-native/components/snap-switch.tsx +46 -0
- package/src/react-native/components/snap-text.tsx +51 -0
- package/src/react-native/components/snap-toggle-group.tsx +127 -0
- package/src/react-native/confetti-overlay.tsx +134 -0
- package/src/react-native/index.tsx +83 -0
- package/src/react-native/snap-view-core.tsx +212 -0
- package/src/react-native/theme.tsx +85 -0
- package/src/react-native/types.ts +38 -0
- package/src/react-native/use-snap-palette.ts +64 -0
- package/src/react-native/v1/snap-view.tsx +229 -0
- package/src/react-native/v2/snap-view.tsx +283 -0
- package/src/schemas.ts +68 -17
- package/src/server/parseRequest.ts +68 -9
- package/src/server/verify.ts +2 -0
- package/src/ui/README.md +8 -8
- package/src/ui/badge.ts +2 -0
- package/src/ui/bar-chart.ts +38 -0
- package/src/ui/button.ts +1 -1
- package/src/ui/catalog.ts +19 -3
- package/src/ui/cell-grid.ts +49 -0
- package/src/ui/image.ts +1 -1
- package/src/ui/index.ts +6 -0
- package/src/ui/item.ts +1 -1
- package/src/ui/schema.ts +2 -2
- package/src/ui/slider.ts +2 -0
- package/src/ui/text.ts +2 -2
- package/src/validator.ts +251 -2
- package/dist/dataStore.d.ts +0 -12
- package/dist/dataStore.js +0 -35
- package/dist/middleware.d.ts +0 -3
- package/dist/middleware.js +0 -3
- package/dist/react/hooks/use-snap-accent.d.ts +0 -13
- package/dist/react/hooks/use-snap-accent.js +0 -32
- package/src/dataStore.ts +0 -62
- package/src/middleware.ts +0 -7
- 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
|
+
});
|