diff --git a/apps/desktop/src/renderer/src/agent-chat/ref-picker.tsx b/apps/desktop/src/renderer/src/agent-chat/ref-picker.tsx index d40ee40fd..f511a243e 100644 --- a/apps/desktop/src/renderer/src/agent-chat/ref-picker.tsx +++ b/apps/desktop/src/renderer/src/agent-chat/ref-picker.tsx @@ -77,6 +77,7 @@ export function RefPicker({ query, onPick, onClose }: RefPickerProps): React.JSX key={`${result.kind}-${result.id}`} type="button" role="option" + aria-selected={false} className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm hover:bg-accent hover:text-accent-foreground" onClick={() => onPick({ kind: result.kind, ref_id: result.id, label: result.label })} > diff --git a/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx b/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx index 30d306b33..70cce25e8 100644 --- a/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx +++ b/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx @@ -164,7 +164,10 @@ export const TagAutocomplete = ({ }, []) useEffect(() => { - if (autoFocus) setTimeout(() => inputRef.current?.focus(), 100) + if (!autoFocus) return + + const focusTimeout = setTimeout(() => inputRef.current?.focus(), 100) + return () => clearTimeout(focusTimeout) }, [autoFocus]) const addTag = useCallback( @@ -273,7 +276,7 @@ export const TagAutocomplete = ({ onClick={() => addTag(tag)} onMouseEnter={() => setRequestedHighlightedIndex(idx)} className={cn( - 'flex items-center gap-2 rounded-sm py-2 px-3 mx-1 my-0.5 text-left transition-colors', + 'flex items-center gap-2 rounded-sm py-2 px-3 mx-1 my-0.5 text-start transition-colors', highlightedIndex === idx ? 'bg-[var(--tint)]/[0.03]' : 'hover:bg-foreground/[0.03]' )} > @@ -320,7 +323,7 @@ export const TagAutocomplete = ({ onClick={() => addTag(tag.name)} onMouseEnter={() => setRequestedHighlightedIndex(idx)} className={cn( - 'flex items-center gap-2 rounded-sm py-2 px-3 mx-1 my-0.5 text-left transition-colors', + 'flex items-center gap-2 rounded-sm py-2 px-3 mx-1 my-0.5 text-start transition-colors', highlightedIndex === idx ? 'bg-foreground/[0.03]' : 'hover:bg-foreground/[0.03]' )} > @@ -349,7 +352,7 @@ export const TagAutocomplete = ({ onClick={() => addTag(normalized)} onMouseEnter={() => setRequestedHighlightedIndex(idx)} className={cn( - 'flex items-center w-full py-2 px-3 gap-1.5 border-t border-border/40 text-left transition-colors', + 'flex items-center w-full py-2 px-3 gap-1.5 border-t border-border/40 text-start transition-colors', highlightedIndex === idx ? 'bg-foreground/[0.03]' : 'hover:bg-foreground/[0.03]' )} > diff --git a/apps/desktop/src/renderer/src/components/journal/note-drawer.tsx b/apps/desktop/src/renderer/src/components/journal/note-drawer.tsx index 3a0a7d593..27731bbe3 100644 --- a/apps/desktop/src/renderer/src/components/journal/note-drawer.tsx +++ b/apps/desktop/src/renderer/src/components/journal/note-drawer.tsx @@ -69,10 +69,11 @@ export const NoteDrawer = memo(function NoteDrawer({ // Focus close button when drawer opens useEffect(() => { - if (isOpen && closeButtonRef.current) { - // Small delay to allow animation to start - setTimeout(() => closeButtonRef.current?.focus(), 100) - } + if (!isOpen || !closeButtonRef.current) return + + // Small delay to allow animation to start + const focusTimeout = setTimeout(() => closeButtonRef.current?.focus(), 100) + return () => clearTimeout(focusTimeout) }, [isOpen]) // Handle click outside @@ -106,9 +107,9 @@ export const NoteDrawer = memo(function NoteDrawer({ aria-labelledby="drawer-title" aria-hidden={!isOpen} className={cn( - 'fixed top-0 right-0 bottom-0 z-50', + 'fixed top-0 end-0 bottom-0 z-50', 'w-[40vw] min-w-[360px] max-w-[600px]', - 'bg-card border-l border-border', + 'bg-card border-s border-border', 'shadow-[-4px_0_20px_rgba(0,0,0,0.15)]', 'flex flex-col', 'transform transition-transform duration-250 ease-out', diff --git a/apps/desktop/src/renderer/src/components/note-tree-internal.tsx b/apps/desktop/src/renderer/src/components/note-tree-internal.tsx index 897aa65a5..50efc1180 100644 --- a/apps/desktop/src/renderer/src/components/note-tree-internal.tsx +++ b/apps/desktop/src/renderer/src/components/note-tree-internal.tsx @@ -67,9 +67,11 @@ export function RevealHandler({ } } - setTimeout(() => { + const revealTimeout = setTimeout(() => { onReveal(pendingRevealNoteId) }, 50) + + return () => clearTimeout(revealTimeout) }, [pendingRevealNoteId, noteMap, expandNode, onReveal, onClear]) return null diff --git a/apps/desktop/src/renderer/src/components/sidebar/sortable-project-item.tsx b/apps/desktop/src/renderer/src/components/sidebar/sortable-project-item.tsx index ae4f1e0ad..bf3b6bcd1 100644 --- a/apps/desktop/src/renderer/src/components/sidebar/sortable-project-item.tsx +++ b/apps/desktop/src/renderer/src/components/sidebar/sortable-project-item.tsx @@ -10,7 +10,7 @@ import { SidebarMenuItem, useSidebar } from '@/components/ui/sidebar' -import { useDragContext } from '@/contexts/drag-context' +import { useOptionalDragContext } from '@/contexts/drag-context' import type { Project } from '@/data/tasks-data' import { useT } from '@memry/i18n/renderer' @@ -39,14 +39,8 @@ export const SortableProjectItem = ({ const { t: tPhaseF } = useT('notes') const { isMobile: _isMobile } = useSidebar() - // Try to get drag context - may not be available if not wrapped in DragProvider - let dragState = { isDragging: false } - try { - const context = useDragContext() - dragState = context.dragState - } catch { - // Not in DragProvider context - that's okay, just no drop zone features - } + const dragContext = useOptionalDragContext() + const dragState = dragContext?.dragState ?? { isDragging: false } // Sortable for reordering projects const { @@ -98,7 +92,7 @@ export const SortableProjectItem = ({ > {/* Drop indicator when hovering */} {isOver && ( - + {tPhaseF('phaseF.componentsSidebarSortableProjectItem.dropHere')} )} diff --git a/apps/desktop/src/renderer/src/components/sync/otp-input.tsx b/apps/desktop/src/renderer/src/components/sync/otp-input.tsx index ed68389d6..8eb75b4da 100644 --- a/apps/desktop/src/renderer/src/components/sync/otp-input.tsx +++ b/apps/desktop/src/renderer/src/components/sync/otp-input.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from 'react' +import { useState, useEffect, useCallback } from 'react' import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp' import { Loader2, Clock } from '@/lib/icons' import { useT } from '@memry/i18n/renderer' @@ -28,37 +28,21 @@ function useCountdown( reset: () => void } { const [seconds, setSeconds] = useState(initialSeconds) - const intervalRef = useRef | null>(null) - - const clearTimer = useCallback(() => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } - }, []) useEffect(() => { - if (seconds <= 0 || intervalRef.current) return clearTimer - - intervalRef.current = setInterval(() => { - setSeconds((prev) => { - if (prev <= 1) { - clearTimer() - return 0 - } + if (seconds <= 0) return - return prev - 1 - }) + const countdownTimeout = setTimeout(() => { + setSeconds((prev) => Math.max(prev - 1, 0)) }, 1000) - return clearTimer - }, [seconds, clearTimer]) + return () => clearTimeout(countdownTimeout) + }, [seconds]) const reset = useCallback(() => { onResend() - clearTimer() setSeconds(60) - }, [onResend, clearTimer]) + }, [onResend]) return { seconds, canResend: seconds === 0, reset } } diff --git a/apps/desktop/src/renderer/src/components/tasks/due-date-picker.tsx b/apps/desktop/src/renderer/src/components/tasks/due-date-picker.tsx index 2e15d642a..67b4566cf 100644 --- a/apps/desktop/src/renderer/src/components/tasks/due-date-picker.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/due-date-picker.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useCallback, useRef } from 'react' +import { useState, useMemo, useEffect, useCallback, useRef, useId } from 'react' import { Calendar as CalendarIcon, Star, X, Clock, Plus, Sun } from '@/lib/icons' import { Button } from '@/components/ui/button' @@ -176,6 +176,7 @@ export const DueDatePicker = ({ const [inputValue, setInputValue] = useState('') // Track input value for conditional shortcuts const triggerRef = useRef(null) const naturalDateInputRef = useRef(null) + const pickerContentId = useId() const quickOptions = useMemo(() => getQuickDateOptions(), []) @@ -323,22 +324,23 @@ export const DueDatePicker = ({ ref={triggerRef} variant="outline" role="combobox" + aria-controls={pickerContentId} aria-expanded={isOpen} aria-label={tPhaseF('phaseF.componentsTasksDueDatePicker.selectDueDate')} className={cn( - 'w-full justify-start text-left font-normal', + 'w-full justify-start text-start font-normal', !date && 'text-muted-foreground', dateDisplay && statusColors[dateDisplay.status], className )} > - + {date ? ( {dateDisplay?.text} - {time && · {formatTime(time)}} + {time && · {formatTime(time)}} {dateDisplay?.status === 'overdue' && ( - + · {tPhaseF('phaseF.componentsTasksDueDatePicker.overdue')} )} @@ -349,7 +351,7 @@ export const DueDatePicker = ({ - + {!showCalendar ? (
{/* Natural Language Input */} diff --git a/apps/desktop/src/renderer/src/components/tasks/repeat-picker.tsx b/apps/desktop/src/renderer/src/components/tasks/repeat-picker.tsx index 676fb8b81..65576a235 100644 --- a/apps/desktop/src/renderer/src/components/tasks/repeat-picker.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/repeat-picker.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react' +import { useState, useMemo, useCallback, useId } from 'react' import { RefreshCw, ChevronDown, Check } from '@/lib/icons' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' @@ -37,6 +37,7 @@ export const RepeatPicker = ({ const { t: tPhaseF } = useT('tasks') const { t } = useT('common') const [isOpen, setIsOpen] = useState(false) + const optionsId = useId() // Generate presets based on due date const presets = useMemo(() => getRepeatPresets(dueDate), [dueDate]) @@ -80,6 +81,7 @@ export const RepeatPicker = ({