Add beenvoice mobile companion app with full dark mode support.

Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 22:36:37 -04:00
parent 8a7a8df477
commit 14c880123c
93 changed files with 8849 additions and 7849 deletions
+190
View File
@@ -0,0 +1,190 @@
import { Ionicons } from "@expo/vector-icons";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { DateTimeField } from "@/components/ui/DateTimeField";
import { Input } from "@/components/ui/Input";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { formatCurrency, formatDate } from "@/lib/format";
export type EditableLineItem = {
id?: string;
date: Date;
description: string;
hours: string;
rate: string;
};
type LineItemEditorProps = {
item: EditableLineItem;
currency: string;
expanded: boolean;
onToggle: () => void;
onChange: (patch: Partial<EditableLineItem>) => void;
onRemove: () => void;
};
export function LineItemEditor({
item,
currency,
expanded,
onToggle,
onChange,
onRemove,
}: LineItemEditorProps) {
const { colors } = useAppTheme();
const hours = Number(item.hours) || 0;
const rate = Number(item.rate) || 0;
const amount = hours * rate;
const borderStyle = { borderTopColor: colors.border };
if (!expanded) {
return (
<Pressable
accessibilityRole="button"
onPress={onToggle}
style={({ pressed }) => [styles.row, borderStyle, pressed && styles.rowPressed]}
>
<View style={styles.rowMain}>
<Text style={[styles.rowTitle, { color: colors.foreground }]} numberOfLines={1}>
{item.description.trim() || "Untitled line"}
</Text>
<Text style={[styles.rowSub, { color: colors.mutedForeground }]}>
{formatDate(item.date)} · {hours}h × {formatCurrency(rate, currency)}
</Text>
</View>
<Text style={[styles.rowAmount, { color: colors.foreground }]}>
{formatCurrency(amount, currency)}
</Text>
<Ionicons name="chevron-down" size={16} color={colors.mutedForeground} />
</Pressable>
);
}
return (
<View style={[styles.expanded, borderStyle]}>
<View style={styles.expandedHeader}>
<Text style={[styles.expandedLabel, { color: colors.mutedForeground }]}>Line item</Text>
<Pressable accessibilityRole="button" onPress={onToggle} hitSlop={8}>
<Ionicons name="chevron-up" size={18} color={colors.mutedForeground} />
</Pressable>
</View>
<Input
label="Description"
value={item.description}
onChangeText={(description) => onChange({ description })}
placeholder="What was done"
/>
<View style={styles.inlineRow}>
<View style={styles.inlineField}>
<Input
label="Hours"
value={item.hours}
onChangeText={(hours) => onChange({ hours })}
keyboardType="decimal-pad"
placeholder="0"
/>
</View>
<View style={styles.inlineField}>
<Input
label="Rate"
value={item.rate}
onChangeText={(rate) => onChange({ rate })}
keyboardType="decimal-pad"
placeholder="0"
/>
</View>
</View>
<DateTimeField
label="Date"
mode="date"
value={item.date}
onChange={(date) => onChange({ date })}
/>
<View style={styles.expandedFooter}>
<Text style={[styles.lineTotal, { color: colors.foreground }]}>
{formatCurrency(amount, currency)}
</Text>
<Pressable accessibilityRole="button" onPress={onRemove} style={styles.removeButton}>
<Ionicons name="trash-outline" size={16} color={colors.destructive} />
<Text style={[styles.removeLabel, { color: colors.destructive }]}>Remove</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
gap: spacing.sm,
paddingVertical: spacing.sm,
borderTopWidth: 1,
},
rowPressed: {
opacity: 0.9,
},
rowMain: {
flex: 1,
gap: 2,
},
rowTitle: {
fontFamily: fonts.bodyMedium,
fontSize: 15,
},
rowSub: {
fontFamily: fonts.body,
fontSize: 12,
},
rowAmount: {
fontFamily: fonts.bodySemiBold,
fontSize: 14,
},
expanded: {
gap: spacing.sm,
paddingVertical: spacing.sm,
borderTopWidth: 1,
},
expandedHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
expandedLabel: {
fontFamily: fonts.bodySemiBold,
fontSize: 13,
textTransform: "uppercase",
letterSpacing: 0.3,
},
inlineRow: {
flexDirection: "row",
gap: spacing.md,
},
inlineField: {
flex: 1,
},
expandedFooter: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
lineTotal: {
fontFamily: fonts.bodySemiBold,
fontSize: 16,
},
removeButton: {
flexDirection: "row",
alignItems: "center",
gap: 4,
paddingVertical: spacing.xs,
},
removeLabel: {
fontFamily: fonts.bodyMedium,
fontSize: 13,
},
});