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