+
Currently Assigned To
Who is assigned the next due?
diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx
index a2b1d10..dc79a35 100644
--- a/src/views/Chores/ArchivedTasks.jsx
+++ b/src/views/Chores/ArchivedTasks.jsx
@@ -432,6 +432,16 @@ const ArchivedTasks = () => {
setSelectedChores(newSelection)
}
+ // Press-and-hold on a task card enters multi-select with that task picked
+ const enterMultiSelectWithChore = choreId => {
+ if (!isMultiSelectMode) {
+ setIsMultiSelectMode(true)
+ setSelectedChores(new Set([choreId]))
+ return
+ }
+ toggleChoreSelection(choreId)
+ }
+
const selectAllVisibleChores = () => {
if (finalChores.length > 0) {
setSelectedChores(new Set(finalChores.map(c => c.id)))
@@ -1051,6 +1061,7 @@ const ArchivedTasks = () => {
isMultiSelectMode={isMultiSelectMode}
selectedChores={selectedChores}
toggleChoreSelection={toggleChoreSelection}
+ onLongPressChore={enterMultiSelectWithChore}
/>
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
index 8c955d3..263eba8 100644
--- a/src/views/Chores/ChoreCard.jsx
+++ b/src/views/Chores/ChoreCard.jsx
@@ -108,27 +108,29 @@ const ChoreCard = ({
{getDueDateChipText(chore.nextDueDate, chore, timeFormat)}
-
-
- {getFrequencyIcon(chore)}
- {getRecurrentChipText(chore)}
-
-
+
+ {getFrequencyIcon(chore)}
+ {getRecurrentChipText(chore)}
+
+
+ )}
diff --git a/src/views/Chores/ChoreListView.jsx b/src/views/Chores/ChoreListView.jsx
index 5825a24..69b5abb 100644
--- a/src/views/Chores/ChoreListView.jsx
+++ b/src/views/Chores/ChoreListView.jsx
@@ -18,9 +18,60 @@ import {
} from '@mui/icons-material'
import { Box, Typography } from '@mui/joy'
import { useNavigate } from 'react-router-dom'
+import { useLongPress } from '../../hooks/useLongPress'
import ChoreCard from './ChoreCard'
import CompactChoreCard from './CompactChoreCard'
+/**
+ * One swipeable row. Owns the press-and-hold gesture (multi-select), which
+ * can't live in the render loop because it needs a hook.
+ */
+const ChoreSwipeableItem = ({
+ trailingActions,
+ onClick,
+ onLongPress,
+ longPressEnabled,
+ children,
+ // SwipeableList clones its children to inject list-level config
+ // (listType, fullSwipe, thresholds…), so it has to be passed through.
+ ...listProps
+}) => {
+ const { handlers: longPressHandlers, cancel: cancelLongPress } = useLongPress(
+ onLongPress,
+ { enabled: longPressEnabled },
+ )
+
+ // The swipe list owns the gesture the moment it recognizes a drag — a hold
+ // that turned into a swipe must not also open multi-select.
+ const handleSwipeStart = () => {
+ cancelLongPress()
+ }
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
const ChoreListView = ({
chores,
viewMode,
@@ -34,6 +85,7 @@ const ChoreListView = ({
userProfile,
isOfficialInstance,
toggleMultiSelectMode,
+ onLongPressChore,
showActions = true,
}) => {
const navigate = useNavigate()
@@ -248,7 +300,7 @@ const ChoreListView = ({
return (
{chores.map(chore => (
- {
@@ -258,9 +310,11 @@ const ChoreListView = ({
navigate(`/chores/${chore.id}`)
}
}}
+ longPressEnabled={Boolean(onLongPressChore)}
+ onLongPress={() => onLongPressChore?.(chore.id)}
>
{renderChoreCard(chore)}
-
+
))}
)
diff --git a/src/views/Chores/CompactChoreCard.jsx b/src/views/Chores/CompactChoreCard.jsx
index f92d55d..e760086 100644
--- a/src/views/Chores/CompactChoreCard.jsx
+++ b/src/views/Chores/CompactChoreCard.jsx
@@ -84,7 +84,9 @@ const CompactChoreCard = ({
const parts = []
// Frequency
- parts.push(getRecurrentChipText(chore))
+ if (!['once', 'no_repeat'].includes(chore.frequencyType)) {
+ parts.push(getRecurrentChipText(chore))
+ }
// Assignee
if (chore.assignedTo) {
@@ -408,7 +410,8 @@ const CompactChoreCard = ({
{/* Line 2: Metadata */}
- {getFrequencyIcon(chore)}
+ {!['once', 'no_repeat'].includes(chore.frequencyType) &&
+ getFrequencyIcon(chore)}
{
const [confirmModelConfig, setConfirmModelConfig] = useState({})
const { selectedProject, projectsWithDefault, setSelectedProjectWithCache } =
- useProjectFilter(projects)
+ useProjectFilter(projects, !projectsLoading)
const {
searchTerm,
@@ -143,6 +143,7 @@ const MyChores = () => {
selectedChores,
toggleMultiSelectMode,
toggleChoreSelection,
+ enterMultiSelectWithChore,
selectAllVisibleChores,
clearSelection,
getSelectedChoresData,
@@ -366,6 +367,7 @@ const MyChores = () => {
}
processEffectAsync()
+ // throw new Error('Fake Error to test posthog')
}
}, [
membersLoading,
@@ -570,6 +572,7 @@ const MyChores = () => {
handleBulkArchive,
handleBulkDelete,
handleBulkSkip,
+ handleBulkMoveToProject,
} = useChoreActions({
chores,
filteredChores,
@@ -865,8 +868,8 @@ const MyChores = () => {
[getFilteredChores],
)
- const updateChores = newChore => {
- let newChores = [...chores, newChore]
+ const appendChore = (prev, newChore) => {
+ let newChores = [...prev, newChore]
if (impersonatedUser) {
newChores = newChores.filter(
@@ -874,8 +877,15 @@ const MyChores = () => {
)
}
- setChores(newChores)
- setFilteredChores(newChores)
+ return newChores
+ }
+
+ // Uses functional setState so back-to-back calls (e.g. creating several
+ // voice-captured tasks in a row) each build on the latest state instead of
+ // a closure snapshot taken before earlier calls landed.
+ const updateChores = newChore => {
+ setChores(prev => appendChore(prev, newChore))
+ setFilteredChores(prev => appendChore(prev, newChore))
clearQuickFilters()
}
@@ -1046,12 +1056,22 @@ const MyChores = () => {
+ selectAllVisibleChores(
+ searchTerm?.length > 0 || hasQuickFilters || activeFilterId
+ ? getFilteredChores
+ : null,
+ choreSections,
+ openChoreSections,
+ )
+ }
onClear={clearSelection}
onComplete={handleBulkComplete}
onSkip={handleBulkSkip}
onArchive={handleBulkArchive}
onDelete={handleBulkDelete}
+ onMoveToProject={handleBulkMoveToProject}
+ projects={projects}
showKeyboardShortcuts={showKeyboardShortcuts}
selectAllDisabled={
searchTerm?.length > 0 || hasQuickFilters
@@ -1116,6 +1136,7 @@ const MyChores = () => {
isMultiSelectMode={isMultiSelectMode}
selectedChores={selectedChores}
toggleChoreSelection={toggleChoreSelection}
+ onLongPressChore={enterMultiSelectWithChore}
/>
)}
{viewMode === 'calendar' && (
@@ -1293,6 +1314,7 @@ const MyChores = () => {
isMultiSelectMode={isMultiSelectMode}
selectedChores={selectedChores}
toggleChoreSelection={toggleChoreSelection}
+ onLongPressChore={enterMultiSelectWithChore}
/>
)}
@@ -1373,6 +1395,7 @@ const MyChores = () => {
isMultiSelectMode={isMultiSelectMode}
selectedChores={selectedChores}
toggleChoreSelection={toggleChoreSelection}
+ onLongPressChore={enterMultiSelectWithChore}
/>
diff --git a/src/views/Chores/components/ChoreModals.jsx b/src/views/Chores/components/ChoreModals.jsx
index 4f31ad0..cc23cb0 100644
--- a/src/views/Chores/components/ChoreModals.jsx
+++ b/src/views/Chores/components/ChoreModals.jsx
@@ -1,5 +1,9 @@
import { Capacitor } from '@capacitor/core'
import DateModal from '../../Modals/Inputs/DateModal'
+import DueDatePickerModal, {
+ combineDueDate,
+ splitDueDate,
+} from '../../components/DueDatePickerModal'
import NudgeModal from '../../Modals/Inputs/NudgeModal'
import SelectModal from '../../Modals/Inputs/SelectModal'
import TextModal from '../../Modals/Inputs/TextModal'
@@ -24,13 +28,16 @@ const ChoreModals = ({
return (
<>
{activeModal === 'changeDueDate' && modalChore && (
-
+ onChangeDueDate(combineDueDate(parts)?.toISOString() ?? null)
+ }
+ onRemove={() => onChangeDueDate(null)}
/>
)}
diff --git a/src/views/Chores/components/MultiSelectToolbar.jsx b/src/views/Chores/components/MultiSelectToolbar.jsx
index 975c828..cc8334b 100644
--- a/src/views/Chores/components/MultiSelectToolbar.jsx
+++ b/src/views/Chores/components/MultiSelectToolbar.jsx
@@ -5,11 +5,39 @@ import {
Close,
Delete,
Done,
+ DriveFileMove,
SelectAll,
SkipNext,
} from '@mui/icons-material'
-import { Box, Button, Divider, Typography } from '@mui/joy'
+import {
+ Avatar,
+ Box,
+ Button,
+ Divider,
+ ListItemContent,
+ ListItemDecorator,
+ Menu,
+ MenuItem,
+ Typography,
+} from '@mui/joy'
+import { useRef, useState } from 'react'
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
+import LABEL_COLORS, {
+ getTextColorFromBackgroundColor,
+} from '../../../utils/Colors'
+import { getIconComponent } from '../../../utils/ProjectIcons'
+
+const renderProjectAvatar = (color, icon) => {
+ const bg = color || LABEL_COLORS[0].value
+ const IconComponent = getIconComponent(icon || 'FolderOpen')
+ return (
+
+
+
+ )
+}
const MultiSelectToolbar = ({
isVisible,
@@ -20,9 +48,21 @@ const MultiSelectToolbar = ({
onSkip,
onArchive,
onDelete,
+ onMoveToProject,
+ projects = [],
showKeyboardShortcuts,
selectAllDisabled,
}) => {
+ const [projectMenuAnchor, setProjectMenuAnchor] = useState(null)
+ const projectMenuRef = useRef(null)
+
+ const closeProjectMenu = () => setProjectMenuAnchor(null)
+
+ const handleMoveToProject = project => {
+ closeProjectMenu()
+ onMoveToProject?.(project)
+ }
+
return (
)}
+ {onMoveToProject && (
+ <>
+
+
+ >
+ )}
+
- {/* Sub-panels (voice/scan) own their own confirm action */}
+ {showVoice && (
+
+ )}
+ {showScan && scanState.primaryAction && (
+
+ )}
+ {showScan && scanState.phase === 'processing' && (
+
+ )}
{!showScan && !showVoice && (
- setIsOpen(false)}
- title='Due Date'
- fullWidth={false}
- footer={
- {
- onClear?.()
- setIsOpen(false)
- },
- }
- : undefined
- }
- secondary={{ label: 'Cancel', onClick: () => setIsOpen(false) }}
- primary={{ label: 'Apply', onClick: handleSave }}
- />
+ dueDateOnly={dueDateOnly}
+ dueTime={dueTime}
+ useCustomTime={useCustomTime}
+ onApply={handleSave}
+ onRemove={
+ onClear
+ ? () => {
+ onClear()
+ setIsOpen(false)
+ }
+ : undefined
}
- >
-
- {/* Date shortcuts */}
-
- Quick date
-
-
- {[
- {
- key: 'today',
- label: 'Today',
- icon: ,
- },
- {
- key: 'tomorrow',
- label: 'Tomorrow',
- icon: ,
- },
- {
- key: 'weekend',
- label: 'Weekend',
- icon: ,
- },
- {
- key: 'next-week',
- label: 'Next week',
- icon: ,
- },
- {
- key: 'next-month',
- label: 'Next month',
- icon: ,
- },
- ].map(opt => {
- const dateStr = getQuickScheduleDate(opt.key)
- .toISOString()
- .split('T')[0]
- return (
-
- handleQuickSchedule(opt.key)}
- overlay
- disableIcon
- variant='soft'
- label={
-
- {opt.icon}
- {opt.label}
-
- }
- />
-
- )
- })}
-
-
- {/* Time shortcuts */}
-
- Quick time
-
-
- {[
- {
- time: '09:00',
- label: 'Morning',
- icon: ,
- },
- {
- time: '12:00',
- label: 'Noon',
- icon: ,
- },
- {
- time: '15:00',
- label: 'Afternoon',
- icon: ,
- },
- {
- time: '18:00',
- label: 'Evening',
- icon: ,
- },
- {
- time: '22:00',
- label: 'Night',
- icon: ,
- },
- ].map(opt => (
-
- handleQuickTime(opt.time)}
- overlay
- disableIcon
- variant='soft'
- label={
-
- {opt.icon}
- {opt.label}
-
- }
- />
-
- ))}
-
-
-
-
- ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'][date.getDay()]
- }
- formatMonth={(locale, date) =>
- [
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec',
- ][date.getMonth()]
- }
- />
-
-
- Custom time
-
-
-
-
-
-
-
-
+ />
>
)
}
diff --git a/src/views/components/DueDatePickerModal.jsx b/src/views/components/DueDatePickerModal.jsx
new file mode 100644
index 0000000..c0f47da
--- /dev/null
+++ b/src/views/components/DueDatePickerModal.jsx
@@ -0,0 +1,544 @@
+import {
+ Bedtime,
+ EventNote,
+ LightMode,
+ NextWeek,
+ NightsStay,
+ Today,
+ WbSunny,
+ WbTwilight,
+ Weekend,
+} from '@mui/icons-material'
+import {
+ Box,
+ Button,
+ Checkbox,
+ Input,
+ List,
+ ListItem,
+ Typography,
+} from '@mui/joy'
+import moment from 'moment'
+import { useEffect, useState } from 'react'
+import Calendar from 'react-calendar'
+import ModalActions from '../../components/common/ModalActions'
+import { useLocalization } from '../../contexts/LocalizationContext'
+import { useResponsiveModal } from '../../hooks/useResponsiveModal'
+
+// Split a date-ish value (ISO string / Date) into the parts this picker edits.
+export const splitDueDate = value => {
+ if (!value) {
+ return { dueDateOnly: null, dueTime: null, useCustomTime: false }
+ }
+ const m = moment(value)
+ if (!m.isValid()) {
+ return { dueDateOnly: null, dueTime: null, useCustomTime: false }
+ }
+ const time = m.format('HH:mm')
+ return {
+ dueDateOnly: m.format('YYYY-MM-DD'),
+ dueTime: time,
+ // Midnight is how a date-only value round-trips, so treat it as "anytime"
+ useCustomTime: time !== '00:00',
+ }
+}
+
+// Inverse of splitDueDate — returns a Date, or null when there is no due date.
+export const combineDueDate = ({ dueDateOnly, dueTime, useCustomTime }) => {
+ if (!dueDateOnly) return null
+ const time = useCustomTime && dueTime ? dueTime : '00:00'
+ return moment(`${dueDateOnly} ${time}`, 'YYYY-MM-DD HH:mm').toDate()
+}
+
+export const getQuickScheduleDate = option => {
+ const now = new Date()
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
+
+ switch (option) {
+ case 'today':
+ return today
+ case 'tomorrow': {
+ const tomorrow = new Date(today)
+ tomorrow.setDate(today.getDate() + 1)
+ return tomorrow
+ }
+ case 'weekend': {
+ const weekend = new Date(today)
+ const daysUntilSaturday = (6 - today.getDay() + 7) % 7 || 7
+ weekend.setDate(today.getDate() + daysUntilSaturday)
+ return weekend
+ }
+ case 'next-week': {
+ const nextWeek = new Date(today)
+ const daysUntilMonday = (1 - today.getDay() + 7) % 7 || 7
+ nextWeek.setDate(today.getDate() + daysUntilMonday)
+ return nextWeek
+ }
+ case 'next-month': {
+ const nextMonth = new Date(today)
+ nextMonth.setMonth(today.getMonth() + 1)
+ return nextMonth
+ }
+ default:
+ return today
+ }
+}
+
+const toDateKey = date => moment(date).format('YYYY-MM-DD')
+
+/**
+ * The shared due-date picker UI (quick dates, quick times, calendar, custom
+ * time). Used both by DueDatePickerField and by anything that needs to
+ * reschedule a task — task cards, swipe actions, action menus.
+ */
+const DueDatePickerModal = ({
+ open,
+ onClose,
+ title = 'Due Date',
+ dueDateOnly,
+ dueTime,
+ useCustomTime,
+ onApply,
+ onRemove,
+ applyLabel = 'Apply',
+}) => {
+ const { ResponsiveModal } = useResponsiveModal()
+ const { firstDayOfWeek } = useLocalization()
+
+ // Local buffered state — only committed on Apply
+ const [localDueDateOnly, setLocalDueDateOnly] = useState(dueDateOnly)
+ const [localDueTime, setLocalDueTime] = useState(dueTime)
+ const [localUseCustomTime, setLocalUseCustomTime] = useState(useCustomTime)
+
+ // Sync local state from props whenever the modal opens
+ useEffect(() => {
+ if (open) {
+ setLocalDueDateOnly(dueDateOnly)
+ setLocalDueTime(dueTime)
+ setLocalUseCustomTime(useCustomTime)
+ }
+ }, [open, dueDateOnly, dueTime, useCustomTime])
+
+ const calendarType =
+ firstDayOfWeek === 1
+ ? 'iso8601'
+ : firstDayOfWeek === 6
+ ? 'islamic'
+ : 'gregory'
+
+ const pillListSx = {
+ '--List-gap': '8px',
+ '--ListItem-radius': '20px',
+ }
+
+ const handleQuickSchedule = option => {
+ setLocalDueDateOnly(toDateKey(getQuickScheduleDate(option)))
+ }
+
+ const handleQuickTime = timeStr => {
+ // Tap the active chip again to deselect it
+ if (localUseCustomTime && localDueTime === timeStr) {
+ setLocalUseCustomTime(false)
+ setLocalDueTime(null)
+ return
+ }
+ if (!localDueDateOnly) {
+ setLocalDueDateOnly(toDateKey(new Date()))
+ }
+ setLocalUseCustomTime(true)
+ setLocalDueTime(timeStr)
+ }
+
+ const handleCalendarChange = selected => {
+ if (!selected || Array.isArray(selected)) return
+ setLocalDueDateOnly(moment(selected).format('YYYY-MM-DD'))
+ }
+
+ const handleLocalTimeInputChange = e => {
+ setLocalUseCustomTime(true)
+ setLocalDueTime(e.target.value)
+ }
+
+ const handleSave = () => {
+ onApply?.({
+ dueDateOnly: localDueDateOnly || null,
+ dueTime: localUseCustomTime ? localDueTime || null : null,
+ useCustomTime: Boolean(localUseCustomTime && localDueTime),
+ })
+ }
+
+ return (
+
+ }
+ >
+
+ {/* Date shortcuts */}
+
+ Quick date
+
+
+ {[
+ {
+ key: 'today',
+ label: 'Today',
+ icon: ,
+ },
+ {
+ key: 'tomorrow',
+ label: 'Tomorrow',
+ icon: ,
+ },
+ {
+ key: 'weekend',
+ label: 'Weekend',
+ icon: ,
+ },
+ {
+ key: 'next-week',
+ label: 'Next week',
+ icon: ,
+ },
+ {
+ key: 'next-month',
+ label: 'Next month',
+ icon: ,
+ },
+ ].map(opt => {
+ const dateStr = toDateKey(getQuickScheduleDate(opt.key))
+ return (
+
+ handleQuickSchedule(opt.key)}
+ overlay
+ disableIcon
+ variant='soft'
+ label={
+
+ {opt.icon}
+ {opt.label}
+
+ }
+ />
+
+ )
+ })}
+
+
+ {/* Time shortcuts */}
+
+ Quick time
+
+
+ {[
+ {
+ time: '09:00',
+ label: 'Morning',
+ icon: ,
+ },
+ {
+ time: '12:00',
+ label: 'Noon',
+ icon: ,
+ },
+ {
+ time: '15:00',
+ label: 'Afternoon',
+ icon: ,
+ },
+ {
+ time: '18:00',
+ label: 'Evening',
+ icon: ,
+ },
+ {
+ time: '22:00',
+ label: 'Night',
+ icon: ,
+ },
+ ].map(opt => (
+
+ handleQuickTime(opt.time)}
+ overlay
+ disableIcon
+ variant='soft'
+ label={
+
+ {opt.icon}
+ {opt.label}
+
+ }
+ />
+
+ ))}
+
+
+
+
+ ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'][date.getDay()]
+ }
+ formatMonth={(locale, date) =>
+ [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+ ][date.getMonth()]
+ }
+ />
+
+
+ Custom time
+
+
+
+
+
+
+
+
+ )
+}
+
+export default DueDatePickerModal
diff --git a/src/views/components/ScanToTask/ScanPanel.jsx b/src/views/components/ScanToTask/ScanPanel.jsx
index be7aa02..3cf41a8 100644
--- a/src/views/components/ScanToTask/ScanPanel.jsx
+++ b/src/views/components/ScanToTask/ScanPanel.jsx
@@ -12,7 +12,7 @@ import {
LinearProgress,
Typography,
} from '@mui/joy'
-import { useEffect } from 'react'
+import { useCallback, useEffect, useMemo } from 'react'
import { useScanToTask } from './useScanToTask'
/**
@@ -20,8 +20,20 @@ import { useScanToTask } from './useScanToTask'
*
* Flow: capture → (auto) processing → done [calls onTaskExtracted + onClose]
* → error [retake or cancel]
+ *
+ * The primary action (Capture / Scan Document / Retake) lives in the modal
+ * footer alongside Cancel — the panel reports it up through onStateChange
+ * rather than rendering its own button row. Upload stays inline because it
+ * belongs to the capture surface and drives a hidden input in this subtree.
*/
-const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCapture }) => {
+const ScanPanel = ({
+ open,
+ onTaskExtracted,
+ onClose,
+ onStateChange,
+ initialImageUrl,
+ autoCapture,
+}) => {
const {
isNativeScanner,
phase,
@@ -76,6 +88,51 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCaptur
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [phase, taskResult])
+ const openFilePicker = useCallback(
+ () => fileInputRef.current?.click(),
+ [fileInputRef],
+ )
+
+ // The one action the footer renders for the current phase; null while
+ // processing (nothing to do but wait) and when done (the panel closes)
+ const primaryAction = useMemo(() => {
+ if (phase === 'capture') {
+ if (isNativeScanner) {
+ return {
+ label: 'Scan Document',
+ icon: ,
+ onClick: handleNativeScan,
+ }
+ }
+ if (cameraAvailable) {
+ return { label: 'Capture', icon: , onClick: capture }
+ }
+ // No camera on this device — Upload is the only way forward, so it
+ // graduates from the inline secondary to the footer's primary
+ return {
+ label: 'Upload Photo',
+ icon: ,
+ onClick: openFilePicker,
+ }
+ }
+ if (phase === 'error') {
+ return { label: 'Retake', icon: , onClick: retake }
+ }
+ return null
+ }, [
+ phase,
+ isNativeScanner,
+ cameraAvailable,
+ capture,
+ handleNativeScan,
+ retake,
+ openFilePicker,
+ ])
+
+ useEffect(() => {
+ onStateChange?.({ phase, primaryAction })
+ }, [phase, primaryAction, onStateChange])
+
if (!open) return null
const isProcessing = phase === 'processing'
@@ -111,7 +168,10 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCaptur
-
+
Tap "Scan Document" to open the scanner
@@ -137,65 +197,38 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCaptur