diff --git a/carry-your-live/app/(tabs)/calendar.tsx b/carry-your-live/app/(tabs)/calendar.tsx index 396172d..59a9fca 100644 --- a/carry-your-live/app/(tabs)/calendar.tsx +++ b/carry-your-live/app/(tabs)/calendar.tsx @@ -218,7 +218,22 @@ export default function CalendarScreen() { return ( -
+
router.back()} + style={styles.backButton} + accessible={true} + accessibilityLabel="Back" + > + + + + + } + /> @@ -609,6 +624,9 @@ const createStyles = (theme: ThemeColors) => StyleSheet.create({ emptySubtext: { fontSize: 13, }, + backButton: { + padding: 8, + }, eventCard: { marginHorizontal: 16, marginTop: 6, diff --git a/carry-your-live/app/(tabs)/settings.tsx b/carry-your-live/app/(tabs)/settings.tsx index 6055217..41ce3db 100644 --- a/carry-your-live/app/(tabs)/settings.tsx +++ b/carry-your-live/app/(tabs)/settings.tsx @@ -24,7 +24,7 @@ import { getLastVisitedTab, tabHref } from '@/utils/tabHistory'; export default function SettingsScreen() { const router = useRouter(); - const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, tagReminderPreferences, setTagReminderPreferences, apiUrl, accentColor, setAccentColor, todoAheadDays, setTodoAheadDays, showCompleted, setShowCompleted, deleteBySwipe, setDeleteBySwipe } = useSettings(); + const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, tagReminderPreferences, setTagReminderPreferences, apiUrl, accentColor, setAccentColor, accentOpacity, setAccentOpacity, todoAheadDays, setTodoAheadDays, showCompleted, setShowCompleted, deleteBySwipe, setDeleteBySwipe } = useSettings(); const categories = useCategories(); const [picker, setPicker] = useState(null); const [reminderTagPicker, setReminderTagPicker] = useState(null); @@ -35,6 +35,7 @@ export default function SettingsScreen() { const [serverUrlVisible, setServerUrlVisible] = useState(false); const [customAccentVisible, setCustomAccentVisible] = useState(false); const [draftAccent, setDraftAccent] = useState(accentColor); + const [draftAccentOpacity, setDraftAccentOpacity] = useState(accentOpacity); const [syncSubtitle, setSyncSubtitle] = useState('Checking...'); const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check'); @@ -393,18 +394,18 @@ export default function SettingsScreen() { - + { setDraftAccent(color); setDraftAccentOpacity(opacity); }} /> { setDraftAccent(DEFAULT_ACCENT); setAccentColor(DEFAULT_ACCENT); }} + onPress={() => { setDraftAccent(DEFAULT_ACCENT); setDraftAccentOpacity(DEFAULT_ACCENT_OPACITY); setAccentColor(DEFAULT_ACCENT); setAccentOpacity(DEFAULT_ACCENT_OPACITY); }} activeOpacity={0.7} > Reset { setAccentColor(draftAccent); setCustomAccentVisible(false); }} + onPress={() => { setAccentColor(draftAccent); setAccentOpacity(draftAccentOpacity); setCustomAccentVisible(false); }} activeOpacity={0.8} > Apply diff --git a/carry-your-live/src/components/ColorWheel.native.tsx b/carry-your-live/src/components/ColorWheel.native.tsx index d4ed304..a284c46 100644 --- a/carry-your-live/src/components/ColorWheel.native.tsx +++ b/carry-your-live/src/components/ColorWheel.native.tsx @@ -70,10 +70,11 @@ function hsvToHex(h: number, s: number, v: number) { interface ColorWheelProps { color: string; - onChange: (color: string) => void; + opacity: number; + onChange: (color: string, opacity: number) => void; } -export default function ColorWheel({ color, onChange }: ColorWheelProps) { +export default function ColorWheel({ color, opacity, onChange }: ColorWheelProps) { const { theme } = useSettings(); const initialHsv = useMemo(() => hexToHsv(color), [color]); const [hue, setHue] = useState(initialHsv.h); @@ -90,8 +91,8 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) { useEffect(() => { const newColor = hsvToHex(hue, saturation, value); - onChange(newColor); - }, [hue, saturation, value, onChange]); + onChange(newColor, opacity); + }, [hue, saturation, value, opacity, onChange]); const updateFromWheel = (locationX: number, locationY: number) => { const center = WHEEL_SIZE / 2; diff --git a/carry-your-live/src/components/ColorWheel.web.tsx b/carry-your-live/src/components/ColorWheel.web.tsx index a094b15..2d00edd 100644 --- a/carry-your-live/src/components/ColorWheel.web.tsx +++ b/carry-your-live/src/components/ColorWheel.web.tsx @@ -69,10 +69,11 @@ function hsvToHex(h: number, s: number, v: number) { interface ColorWheelProps { color: string; - onChange: (color: string) => void; + opacity: number; + onChange: (color: string, opacity: number) => void; } -export default function ColorWheel({ color, onChange }: ColorWheelProps) { +export default function ColorWheel({ color, opacity, onChange }: ColorWheelProps) { const { theme } = useSettings(); const initialHsv = useMemo(() => hexToHsv(color), [color]); const [hue, setHue] = useState(initialHsv.h); @@ -89,8 +90,8 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) { useEffect(() => { const newColor = hsvToHex(hue, saturation, value); - onChange(newColor); - }, [hue, saturation, value, onChange]); + onChange(newColor, opacity); + }, [hue, saturation, value, opacity, onChange]); const handleWheelMouseDown = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); diff --git a/carry-your-live/src/components/ListItem.tsx b/carry-your-live/src/components/ListItem.tsx index e69de29..c51a33f 100644 --- a/carry-your-live/src/components/ListItem.tsx +++ b/carry-your-live/src/components/ListItem.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet, Switch } from 'react-native'; + +export const ListItem = ({ title, subtitle, onPress, leftIcon, rightIcon, disabled, leftElement, rightElement, showChevron }: { + title: string; + subtitle?: string | undefined; + onPress?: () => void; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + disabled?: boolean; + leftElement?: React.ReactNode; + rightElement?: React.ReactNode; + showChevron?: boolean; +}) => { + return ( + + + {leftElement} + {leftIcon} + + + {title} + {subtitle && {subtitle}} + + + {rightElement} + {rightIcon} + + {showChevron && ▶} + + ); +}; + +const styles = StyleSheet.create({ + item: { + padding: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: 'white', + marginVertical: 4, + borderRadius: 8, + }, + left: { + flex: 1, + }, + center: { + flex: 3, + marginHorizontal: 12, + }, + right: { + flex: 1, + textAlign: 'right', + }, + title: { + fontSize: 16, + fontWeight: '500', + }, + subtitle: { + fontSize: 12, + color: '#757575', + marginTop: 2, + }, + chevron: { + fontSize: 14, + color: '#757575', + marginLeft: 4, + }, +}); \ No newline at end of file diff --git a/carry-your-live/src/components/TaskList.tsx b/carry-your-live/src/components/TaskList.tsx index c9f19a0..9e3ea86 100644 --- a/carry-your-live/src/components/TaskList.tsx +++ b/carry-your-live/src/components/TaskList.tsx @@ -66,7 +66,7 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC const itemRefs = useRef>(new Map()); const subtaskRefs = useRef>(new Map()); const dragStateRef = useRef<{ taskId: string; positions: Record; subPositions: Record } | null>(null); - const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; positions: Record; subPositions: Record } | null>(null); + const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; parentSubtaskId: string | null; positions: Record; subPositions: Record } | null>(null); const { modals, openTaskDelete, @@ -334,7 +334,7 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC } }, [findHoverTarget]); - const handleSubtaskDragStart = useCallback(async (subtaskId: string, parentTaskId: string, parentSubtaskId?: string) => { + const handleSubtaskDragStart = useCallback(async (subtaskId: string, parentTaskId: string, parentSubtaskId: string | null = null) => { subtaskDragRef.current = { subtaskId, parentTaskId, parentSubtaskId, ...(await measureAll()) }; }, [measureAll]); @@ -373,7 +373,7 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => { // Only allow dropping on tasks (not subtasks) to match task behavior // Check if target exists in main task positions, not subtask positions - const isTaskTarget = state.positions && target in state.positions; + const isTaskTarget = state.positions != null && target != null && target in state.positions; if (target && isTaskTarget) { // Move subtask to target task - makes it a subtask of that task // If no target or target is a subtask: do nothing (matches task behavior) diff --git a/carry-your-live/src/theme.tsx b/carry-your-live/src/theme.tsx index b803652..b811cec 100644 --- a/carry-your-live/src/theme.tsx +++ b/carry-your-live/src/theme.tsx @@ -57,6 +57,7 @@ export interface ThemeColors { } export const DEFAULT_ACCENT = '#EF5350'; +export const DEFAULT_ACCENT_OPACITY = 1; export const ACCENT_PRESETS: string[] = [ '#EF5350', @@ -74,6 +75,10 @@ export const ACCENT_PRESETS: string[] = [ function parseHex(hex: string): [number, number, number] { const h = hex.replace(/^#/, ''); + // Handle both #RRGGBB (6 digits) and #AARRGGBB (8 digits with alpha) + if (h.length === 8) { + return [parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), parseInt(h.slice(6, 8), 16)]; + } return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; } @@ -101,8 +106,9 @@ function accentTextColor(accent: string): string { return luminance > 0.5 ? '#111111' : '#FFFFFF'; } -export function colors(accent: string = DEFAULT_ACCENT): ThemeColors { +export function colors(accent: string = DEFAULT_ACCENT, opacity: number = 1): ThemeColors { const softAccent = desaturate(accent, 0.35); + const accentWithOpacity = opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent; return { background: '#121212', card: '#1E1E1E', @@ -113,7 +119,7 @@ export function colors(accent: string = DEFAULT_ACCENT): ThemeColors { textSecondary: '#E3E4E8', textFaint: '#C4C6CC', textMuted: '#9C9FA7', - accent, + accent: opacity < 1 ? `${accentWithOpacity}` : accent, accentStrong: mixHex(accent, '#FFFFFF', 0.55), accentSoft: mixHex(softAccent, '#121212', 0.18), accentBorder: mixHex(desaturate(accent, 0.15), '#121212', 0.5), @@ -166,6 +172,7 @@ const STORAGE_KEYS = { reminderPreference: 'settings:reminderPreference', tagReminderPreferences: TAG_REMINDER_PREFERENCES_KEY, accentColor: 'settings:accentColor', + accentOpacity: 'settings:accentOpacity', todoAheadDays: 'settings:todoAheadDays', showCompleted: 'settings:showCompleted', deleteBySwipe: 'settings:deleteBySwipe', @@ -213,6 +220,7 @@ export function SettingsProvider({ children }: { children: ReactNode }) { ); const [apiUrl, setApiUrl] = useStoredSetting(API_URL_KEY, DEFAULT_API_BASE_URL); const [accentColor, setAccentColor] = useStoredSetting(STORAGE_KEYS.accentColor, DEFAULT_ACCENT); + const [accentOpacity, setAccentOpacity] = useStoredSetting(STORAGE_KEYS.accentOpacity, DEFAULT_ACCENT_OPACITY); const [todoAheadDays, setTodoAheadDays] = useStoredSetting(STORAGE_KEYS.todoAheadDays, DEFAULT_TODO_AHEAD_DAYS); const [showCompleted, setShowCompleted] = useStoredSetting(STORAGE_KEYS.showCompleted, false); const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting(STORAGE_KEYS.deleteBySwipe, false);