Skip to content

Commit 113afff

Browse files
committed
feat(renderers): add textarea, time, list, tree renderers and password support
Register new field renderers across all four frameworks (react-web, react-native, sveltekit, vue-quasar) and add password input type to TextField components.
1 parent e8845f3 commit 113afff

25 files changed

Lines changed: 1468 additions & 5 deletions
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { View, Text, Pressable, StyleSheet } from "react-native";
2+
import { useTranslation } from "react-i18next";
3+
import type { FieldRendererProps } from "@ybyra/react";
4+
import { useTheme } from "../theme/context";
5+
import type { Theme } from "../theme/default";
6+
import { ds } from "../support/ds";
7+
8+
export function ListField({ domain, name, value, config, proxy, errors, onChange }: FieldRendererProps) {
9+
const { t } = useTranslation();
10+
const theme = useTheme();
11+
const styles = createStyles(theme);
12+
if (proxy.hidden) return null;
13+
14+
const fieldLabel = t(`${domain}.fields.${name}`, { defaultValue: name });
15+
const items = Array.isArray(value) ? (value as Record<string, unknown>[]) : [];
16+
const reorderable = config.attrs.reorderable === true;
17+
18+
const remove = (index: number) => {
19+
onChange(items.filter((_, i) => i !== index));
20+
};
21+
22+
const add = () => {
23+
onChange([...items, {}]);
24+
};
25+
26+
const moveUp = (index: number) => {
27+
if (index === 0) return;
28+
const next = [...items];
29+
[next[index - 1], next[index]] = [next[index], next[index - 1]];
30+
onChange(next);
31+
};
32+
33+
const moveDown = (index: number) => {
34+
if (index >= items.length - 1) return;
35+
const next = [...items];
36+
[next[index], next[index + 1]] = [next[index + 1], next[index]];
37+
onChange(next);
38+
};
39+
40+
return (
41+
<View style={styles.container} {...ds(`ListField:${name}`)}>
42+
<Text style={styles.label}>{fieldLabel}</Text>
43+
<View style={[styles.list, errors.length > 0 && styles.listError]}>
44+
{items.map((item, index) => (
45+
<View key={index} style={styles.row}>
46+
<Text style={styles.rowIndex}>#{index + 1}</Text>
47+
<Text style={styles.rowPreview} numberOfLines={1}>
48+
{Object.values(item).filter(Boolean).join(", ") || "—"}
49+
</Text>
50+
<View style={styles.rowActions}>
51+
{reorderable && (
52+
<>
53+
<Pressable onPress={() => moveUp(index)} disabled={proxy.disabled || index === 0}>
54+
<Text style={styles.btnText}></Text>
55+
</Pressable>
56+
<Pressable onPress={() => moveDown(index)} disabled={proxy.disabled || index >= items.length - 1}>
57+
<Text style={styles.btnText}></Text>
58+
</Pressable>
59+
</>
60+
)}
61+
{!proxy.disabled && (
62+
<Pressable onPress={() => remove(index)}>
63+
<Text style={[styles.btnText, styles.btnDestructive]}>×</Text>
64+
</Pressable>
65+
)}
66+
</View>
67+
</View>
68+
))}
69+
</View>
70+
{!proxy.disabled && (
71+
<Pressable style={styles.addBtn} onPress={add}>
72+
<Text style={styles.addBtnText}>+</Text>
73+
</Pressable>
74+
)}
75+
<View style={styles.errorSlot}>
76+
{errors.map((error, i) => (
77+
<Text key={i} style={styles.error}>{error}</Text>
78+
))}
79+
</View>
80+
</View>
81+
);
82+
}
83+
84+
const createStyles = (theme: Theme) => StyleSheet.create({
85+
container: {
86+
paddingHorizontal: theme.spacing.xs,
87+
},
88+
label: {
89+
fontSize: theme.fontSize.sm,
90+
fontWeight: theme.fontWeight.semibold,
91+
marginBottom: theme.spacing.xs,
92+
color: theme.colors.foreground,
93+
},
94+
list: {
95+
borderWidth: 1,
96+
borderColor: theme.colors.input,
97+
borderRadius: theme.borderRadius.md,
98+
overflow: "hidden",
99+
},
100+
listError: {
101+
borderColor: theme.colors.destructive,
102+
},
103+
row: {
104+
flexDirection: "row",
105+
alignItems: "center",
106+
gap: theme.spacing.xs,
107+
paddingHorizontal: theme.spacing.md,
108+
paddingVertical: theme.spacing.xs,
109+
borderBottomWidth: 1,
110+
borderBottomColor: theme.colors.input,
111+
backgroundColor: theme.colors.card,
112+
},
113+
rowIndex: {
114+
fontSize: theme.fontSize.xs,
115+
color: theme.colors.mutedForeground,
116+
minWidth: 24,
117+
},
118+
rowPreview: {
119+
flex: 1,
120+
fontSize: theme.fontSize.sm,
121+
color: theme.colors.cardForeground,
122+
},
123+
rowActions: {
124+
flexDirection: "row",
125+
gap: 8,
126+
},
127+
btnText: {
128+
fontSize: theme.fontSize.md,
129+
color: theme.colors.cardForeground,
130+
paddingHorizontal: 4,
131+
},
132+
btnDestructive: {
133+
color: theme.colors.destructive,
134+
},
135+
addBtn: {
136+
marginTop: theme.spacing.xs,
137+
borderWidth: 1,
138+
borderStyle: "dashed",
139+
borderColor: theme.colors.input,
140+
borderRadius: theme.borderRadius.md,
141+
paddingVertical: theme.spacing.xs,
142+
alignItems: "center",
143+
},
144+
addBtnText: {
145+
fontSize: theme.fontSize.md,
146+
color: theme.colors.mutedForeground,
147+
},
148+
errorSlot: {
149+
minHeight: 20,
150+
marginTop: 2,
151+
},
152+
error: {
153+
fontSize: theme.fontSize.xs,
154+
color: theme.colors.destructive,
155+
},
156+
});

packages/react-native/src/renderers/TextField.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useTheme } from "../theme/context";
55
import type { Theme } from "../theme/default";
66
import { ds } from "../support/ds";
77

8-
export function TextField({ domain, name, value, proxy, errors, onChange, onBlur, onFocus }: FieldRendererProps) {
8+
export function TextField({ domain, name, value, config, proxy, errors, onChange, onBlur, onFocus }: FieldRendererProps) {
99
const { t, i18n } = useTranslation();
1010
const theme = useTheme();
1111
const styles = createStyles(theme);
@@ -27,6 +27,7 @@ export function TextField({ domain, name, value, proxy, errors, onChange, onBlur
2727
editable={!proxy.disabled}
2828
placeholder={placeholder}
2929
placeholderTextColor={theme.colors.mutedForeground}
30+
secureTextEntry={config.kind === "password"}
3031
/>
3132
<View style={styles.errorSlot}>
3233
{errors.map((error, i) => (
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { View, Text, TextInput, StyleSheet } from "react-native";
2+
import { useTranslation } from "react-i18next";
3+
import type { FieldRendererProps } from "@ybyra/react";
4+
import { useTheme } from "../theme/context";
5+
import type { Theme } from "../theme/default";
6+
import { ds } from "../support/ds";
7+
8+
export function TextareaField({ domain, name, value, config, proxy, errors, onChange, onBlur, onFocus }: FieldRendererProps) {
9+
const { t, i18n } = useTranslation();
10+
const theme = useTheme();
11+
const styles = createStyles(theme);
12+
if (proxy.hidden) return null;
13+
14+
const fieldLabel = t(`${domain}.fields.${name}`, { defaultValue: name });
15+
const placeholderKey = `${domain}.fields.${name}.placeholder`;
16+
const placeholder = i18n.exists(placeholderKey) ? t(placeholderKey) : undefined;
17+
const numberOfLines = proxy.height || config.form.height || 3;
18+
19+
return (
20+
<View style={styles.container} {...ds(`TextareaField:${name}`)}>
21+
<Text style={styles.label}>{fieldLabel}</Text>
22+
<TextInput
23+
style={[styles.input, proxy.disabled && styles.inputDisabled, errors.length > 0 && styles.inputError]}
24+
value={String(value ?? "")}
25+
onChangeText={onChange}
26+
onBlur={onBlur}
27+
onFocus={onFocus}
28+
editable={!proxy.disabled}
29+
placeholder={placeholder}
30+
placeholderTextColor={theme.colors.mutedForeground}
31+
multiline
32+
numberOfLines={numberOfLines}
33+
textAlignVertical="top"
34+
/>
35+
<View style={styles.errorSlot}>
36+
{errors.map((error, i) => (
37+
<Text key={i} style={styles.error}>{error}</Text>
38+
))}
39+
</View>
40+
</View>
41+
);
42+
}
43+
44+
const createStyles = (theme: Theme) => StyleSheet.create({
45+
container: {
46+
paddingHorizontal: theme.spacing.xs,
47+
},
48+
label: {
49+
fontSize: theme.fontSize.sm,
50+
fontWeight: theme.fontWeight.semibold,
51+
marginBottom: theme.spacing.xs,
52+
color: theme.colors.foreground,
53+
},
54+
input: {
55+
borderWidth: 1,
56+
borderColor: theme.colors.input,
57+
borderRadius: theme.borderRadius.md,
58+
paddingHorizontal: theme.spacing.md,
59+
paddingVertical: 10,
60+
fontSize: theme.fontSize.md,
61+
backgroundColor: theme.colors.card,
62+
color: theme.colors.cardForeground,
63+
},
64+
inputDisabled: {
65+
backgroundColor: theme.colors.muted,
66+
color: theme.colors.mutedForeground,
67+
},
68+
inputError: {
69+
borderColor: theme.colors.destructive,
70+
},
71+
errorSlot: {
72+
minHeight: 20,
73+
marginTop: 2,
74+
},
75+
error: {
76+
fontSize: theme.fontSize.xs,
77+
color: theme.colors.destructive,
78+
},
79+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { View, Text, TextInput, StyleSheet } from "react-native";
2+
import { useTranslation } from "react-i18next";
3+
import type { FieldRendererProps } from "@ybyra/react";
4+
import { useTheme } from "../theme/context";
5+
import type { Theme } from "../theme/default";
6+
import { ds } from "../support/ds";
7+
8+
export function TimeField({ domain, name, value, proxy, errors, onChange, onBlur, onFocus }: FieldRendererProps) {
9+
const { t, i18n } = useTranslation();
10+
const theme = useTheme();
11+
const styles = createStyles(theme);
12+
if (proxy.hidden) return null;
13+
14+
const fieldLabel = t(`${domain}.fields.${name}`, { defaultValue: name });
15+
const placeholderKey = `${domain}.fields.${name}.placeholder`;
16+
const placeholder = i18n.exists(placeholderKey) ? t(placeholderKey) : "HH:MM";
17+
18+
return (
19+
<View style={styles.container} {...ds(`TimeField:${name}`)}>
20+
<Text style={styles.label}>{fieldLabel}</Text>
21+
<TextInput
22+
style={[styles.input, proxy.disabled && styles.inputDisabled, errors.length > 0 && styles.inputError]}
23+
value={String(value ?? "")}
24+
onChangeText={onChange}
25+
onBlur={onBlur}
26+
onFocus={onFocus}
27+
editable={!proxy.disabled}
28+
placeholder={placeholder}
29+
placeholderTextColor={theme.colors.mutedForeground}
30+
keyboardType="numbers-and-punctuation"
31+
/>
32+
<View style={styles.errorSlot}>
33+
{errors.map((error, i) => (
34+
<Text key={i} style={styles.error}>{error}</Text>
35+
))}
36+
</View>
37+
</View>
38+
);
39+
}
40+
41+
const createStyles = (theme: Theme) => StyleSheet.create({
42+
container: {
43+
paddingHorizontal: theme.spacing.xs,
44+
},
45+
label: {
46+
fontSize: theme.fontSize.sm,
47+
fontWeight: theme.fontWeight.semibold,
48+
marginBottom: theme.spacing.xs,
49+
color: theme.colors.foreground,
50+
},
51+
input: {
52+
borderWidth: 1,
53+
borderColor: theme.colors.input,
54+
borderRadius: theme.borderRadius.md,
55+
paddingHorizontal: theme.spacing.md,
56+
paddingVertical: 10,
57+
fontSize: theme.fontSize.md,
58+
backgroundColor: theme.colors.card,
59+
color: theme.colors.cardForeground,
60+
},
61+
inputDisabled: {
62+
backgroundColor: theme.colors.muted,
63+
color: theme.colors.mutedForeground,
64+
},
65+
inputError: {
66+
borderColor: theme.colors.destructive,
67+
},
68+
errorSlot: {
69+
minHeight: 20,
70+
marginTop: 2,
71+
},
72+
error: {
73+
fontSize: theme.fontSize.xs,
74+
color: theme.colors.destructive,
75+
},
76+
});

0 commit comments

Comments
 (0)