Fix sticky tasks toolbar layout
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-20 11:37:20 +01:00
parent a916bf8c4d
commit 3c2ebdbc0e
2 changed files with 259 additions and 256 deletions

View File

@@ -555,6 +555,7 @@ html.guest-theme.dark {
position: sticky;
top: calc(env(safe-area-inset-top, 0px) + 76px);
z-index: 45;
width: 100%;
}
@media (prefers-reduced-motion: reduce) {

View File

@@ -618,6 +618,254 @@ export default function MobileEventTasksPage() {
}
}
const taskPanel = assignedTasks.length === 0 ? (
<YStack space="$2">
<MobileCard space="$2">
<Text fontSize={13} fontWeight="700" color={text}>
{t('events.tasks.emptyTitle', 'No photo tasks yet')}
</Text>
<Text fontSize={12} color={muted}>
{t('events.tasks.emptyBody', 'Create photo tasks or import a pack for your event.')}
</Text>
<XStack space="$2">
<CTAButton
label={t('events.tasks.emptyActionTask', 'Add photo task')}
onPress={() => setShowTaskSheet(true)}
fullWidth={false}
/>
<CTAButton
label={t('events.tasks.emptyActionPack', 'Import photo task pack')}
tone="ghost"
onPress={() => setShowCollectionSheet(true)}
fullWidth={false}
/>
</XStack>
</MobileCard>
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
onPress={() => setShowTaskSheet(true)}
title={
<XStack alignItems="center" space="$2">
<YStack
width={28}
height={28}
borderRadius={14}
backgroundColor={primary}
alignItems="center"
justifyContent="center"
>
<Plus size={14} color={surface} />
</YStack>
<Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')}
</Text>
</XStack>
}
subTitle={
<Text fontSize={11.5} color={muted}>
{t('events.tasks.addTaskHint', 'Erstelle eine neue Fotoaufgabe für dieses Event.')}
</Text>
}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={16} color={muted} />}
/>
</YGroup.Item>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
onPress={() => setShowCollectionSheet(true)}
title={
<XStack alignItems="center" space="$2">
<YStack
width={28}
height={28}
borderRadius={14}
backgroundColor={primary}
alignItems="center"
justifyContent="center"
>
<Plus size={14} color={surface} />
</YStack>
<Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.import', 'Fotoaufgabenpaket importieren')}
</Text>
</XStack>
}
subTitle={
<Text fontSize={11.5} color={muted}>
{t('events.tasks.importHint', 'Nutze vordefinierte Pakete für deinen Event-Typ.')}
</Text>
}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={16} color={muted} />}
/>
</YGroup.Item>
</YGroup>
</YStack>
) : (
<YStack space="$2">
<YStack ref={assignedRef} />
<Text fontSize="$sm" color={muted}>
{t('events.tasks.count', '{{count}} photo tasks', { count: filteredTasks.length })}
</Text>
{selectionMode ? (
<MobileCard padding="$2.5" space="$2">
<Text fontSize={12} fontWeight="700" color={text}>
{t('events.tasks.selectionCount', '{{count}} ausgewählt', { count: selectedTaskIds.size })}
</Text>
<XStack space="$2">
<CTAButton
label={t('events.tasks.bulkRemove', 'Auswahl löschen')}
tone="danger"
fullWidth={false}
disabled={selectedTaskIds.size === 0 || bulkDeleteBusy}
onPress={() => setBulkDeleteOpen(true)}
/>
<CTAButton
label={t('events.tasks.bulkCancel', 'Auswahl beenden')}
tone="ghost"
fullWidth={false}
onPress={() => clearSelection()}
/>
</XStack>
</MobileCard>
) : null}
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
{filteredTasks.map((task, idx) => (
<YGroup.Item key={task.id}>
<ListItem
hoverTheme
pressTheme
onPress={() => handleTaskPress(task)}
onPointerDown={() => startLongPress(task.id)}
onPointerUp={cancelLongPress}
onPointerLeave={cancelLongPress}
onPointerCancel={cancelLongPress}
title={
<XStack alignItems="center" space="$2">
{selectionMode ? (
<Checkbox
size="$3"
checked={selectedTaskIds.has(task.id)}
onCheckedChange={() => toggleSelectedTask(task.id)}
onPress={(event: any) => event?.stopPropagation?.()}
aria-label={t('events.tasks.select', 'Select photo task')}
>
<Checkbox.Indicator>
<Check size={14} color={text} />
</Checkbox.Indicator>
</Checkbox>
) : null}
<Text fontSize={12.5} fontWeight="600" color={text}>
{task.title}
</Text>
</XStack>
}
subTitle={
task.description ? (
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{task.description}
</Text>
) : null
}
iconAfter={
selectionMode ? null : (
<XStack space="$2" alignItems="center">
{task.emotion ? (
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
) : null}
<Button
size="$2"
circular
backgroundColor={dangerBg}
borderWidth={1}
borderColor={`${danger}33`}
icon={<Trash2 size={14} color={dangerText} />}
aria-label={t('events.tasks.remove', 'Remove photo task')}
disabled={busyId === task.id}
onPress={(event: any) => {
event?.stopPropagation?.();
setDeleteCandidate(task);
}}
/>
<ChevronRight size={14} color={subtle} />
</XStack>
)
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
</YGroup.Item>
))}
</YGroup>
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
<YStack ref={libraryRef} />
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.library', 'Weitere Fotoaufgaben')}
</Text>
<Pressable onPress={() => setShowCollectionSheet(true)}>
<Text fontSize={12} fontWeight="600" color={primary}>
{t('events.tasks.import', 'Import photo task pack')}
</Text>
</Pressable>
</XStack>
<Pressable onPress={() => setExpandedLibrary((prev) => !prev)}>
<Text fontSize={12} fontWeight="600" color={primary}>
{expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')}
</Text>
</Pressable>
{library.length === 0 ? (
<Text fontSize={12} fontWeight="500" color={subtle}>
{t('events.tasks.libraryEmpty', 'Keine weiteren Fotoaufgaben verfügbar.')}
</Text>
) : (
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
<YGroup.Item key={`lib-${task.id}`}>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{task.title}
</Text>
}
subTitle={
task.description ? (
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{task.description}
</Text>
) : null
}
iconAfter={
<XStack space="$1.5" alignItems="center">
<Pressable onPress={() => quickAssign(task.id)}>
<XStack alignItems="center" space="$1">
<Plus size={14} color={primary} />
<Text fontSize={12} fontWeight="600" color={primary}>
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
</Text>
</XStack>
</Pressable>
<ChevronRight size={14} color={subtle} />
</XStack>
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
</YGroup.Item>
))}
</YGroup>
)}
</YStack>
);
return (
<MobileShell
activeTab="tasks"
@@ -715,7 +963,13 @@ export default function MobileEventTasksPage() {
</MobileCard>
) : null}
{!loading ? (
{loading ? (
<YStack space="$2">
{Array.from({ length: 4 }).map((_, idx) => (
<SkeletonCard key={`tsk-${idx}`} height={70} />
))}
</YStack>
) : (
<YStack space="$2">
<Card
borderRadius={22}
@@ -774,7 +1028,7 @@ export default function MobileEventTasksPage() {
</YStack>
</Card>
<YStack className="admin-sticky-toolbar" width="100%">
<div className="admin-sticky-toolbar">
<Card
borderRadius={20}
borderWidth={2}
@@ -820,261 +1074,9 @@ export default function MobileEventTasksPage() {
</Pressable>
</XStack>
</Card>
</YStack>
</YStack>
) : null}
</div>
{loading ? (
<YStack space="$2">
{Array.from({ length: 4 }).map((_, idx) => (
<SkeletonCard key={`tsk-${idx}`} height={70} />
))}
</YStack>
) : assignedTasks.length === 0 ? (
<YStack space="$2">
<MobileCard space="$2">
<Text fontSize={13} fontWeight="700" color={text}>
{t('events.tasks.emptyTitle', 'No photo tasks yet')}
</Text>
<Text fontSize={12} color={muted}>
{t('events.tasks.emptyBody', 'Create photo tasks or import a pack for your event.')}
</Text>
<XStack space="$2">
<CTAButton
label={t('events.tasks.emptyActionTask', 'Add photo task')}
onPress={() => setShowTaskSheet(true)}
fullWidth={false}
/>
<CTAButton
label={t('events.tasks.emptyActionPack', 'Import photo task pack')}
tone="ghost"
onPress={() => setShowCollectionSheet(true)}
fullWidth={false}
/>
</XStack>
</MobileCard>
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
onPress={() => setShowTaskSheet(true)}
title={
<XStack alignItems="center" space="$2">
<YStack
width={28}
height={28}
borderRadius={14}
backgroundColor={primary}
alignItems="center"
justifyContent="center"
>
<Plus size={14} color={surface} />
</YStack>
<Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.addTask', 'Fotoaufgabe hinzufügen')}
</Text>
</XStack>
}
subTitle={
<Text fontSize={11.5} color={muted}>
{t('events.tasks.addTaskHint', 'Erstelle eine neue Fotoaufgabe für dieses Event.')}
</Text>
}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={16} color={muted} />}
/>
</YGroup.Item>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
onPress={() => setShowCollectionSheet(true)}
title={
<XStack alignItems="center" space="$2">
<YStack
width={28}
height={28}
borderRadius={14}
backgroundColor={primary}
alignItems="center"
justifyContent="center"
>
<Plus size={14} color={surface} />
</YStack>
<Text fontSize={12.5} fontWeight="700" color={text}>
{t('events.tasks.import', 'Fotoaufgabenpaket importieren')}
</Text>
</XStack>
}
subTitle={
<Text fontSize={11.5} color={muted}>
{t('events.tasks.importHint', 'Nutze vordefinierte Pakete für deinen Event-Typ.')}
</Text>
}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={16} color={muted} />}
/>
</YGroup.Item>
</YGroup>
</YStack>
) : (
<YStack space="$2">
<YStack ref={assignedRef} />
<Text fontSize="$sm" color={muted}>
{t('events.tasks.count', '{{count}} photo tasks', { count: filteredTasks.length })}
</Text>
{selectionMode ? (
<MobileCard padding="$2.5" space="$2">
<Text fontSize={12} fontWeight="700" color={text}>
{t('events.tasks.selectionCount', '{{count}} ausgewählt', { count: selectedTaskIds.size })}
</Text>
<XStack space="$2">
<CTAButton
label={t('events.tasks.bulkRemove', 'Auswahl löschen')}
tone="danger"
fullWidth={false}
disabled={selectedTaskIds.size === 0 || bulkDeleteBusy}
onPress={() => setBulkDeleteOpen(true)}
/>
<CTAButton
label={t('events.tasks.bulkCancel', 'Auswahl beenden')}
tone="ghost"
fullWidth={false}
onPress={() => clearSelection()}
/>
</XStack>
</MobileCard>
) : null}
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
{filteredTasks.map((task, idx) => (
<YGroup.Item key={task.id}>
<ListItem
hoverTheme
pressTheme
onPress={() => handleTaskPress(task)}
onPointerDown={() => startLongPress(task.id)}
onPointerUp={cancelLongPress}
onPointerLeave={cancelLongPress}
onPointerCancel={cancelLongPress}
title={
<XStack alignItems="center" space="$2">
{selectionMode ? (
<Checkbox
size="$3"
checked={selectedTaskIds.has(task.id)}
onCheckedChange={() => toggleSelectedTask(task.id)}
onPress={(event: any) => event?.stopPropagation?.()}
aria-label={t('events.tasks.select', 'Select photo task')}
>
<Checkbox.Indicator>
<Check size={14} color={text} />
</Checkbox.Indicator>
</Checkbox>
) : null}
<Text fontSize={12.5} fontWeight="600" color={text}>
{task.title}
</Text>
</XStack>
}
subTitle={
task.description ? (
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{task.description}
</Text>
) : null
}
iconAfter={
selectionMode ? null : (
<XStack space="$2" alignItems="center">
{task.emotion ? (
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
) : null}
<Button
size="$2"
circular
backgroundColor={dangerBg}
borderWidth={1}
borderColor={`${danger}33`}
icon={<Trash2 size={14} color={dangerText} />}
aria-label={t('events.tasks.remove', 'Remove photo task')}
disabled={busyId === task.id}
onPress={(event: any) => {
event?.stopPropagation?.();
setDeleteCandidate(task);
}}
/>
<ChevronRight size={14} color={subtle} />
</XStack>
)
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
</YGroup.Item>
))}
</YGroup>
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
<YStack ref={libraryRef} />
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.library', 'Weitere Fotoaufgaben')}
</Text>
<Pressable onPress={() => setShowCollectionSheet(true)}>
<Text fontSize={12} fontWeight="600" color={primary}>
{t('events.tasks.import', 'Import photo task pack')}
</Text>
</Pressable>
</XStack>
<Pressable onPress={() => setExpandedLibrary((prev) => !prev)}>
<Text fontSize={12} fontWeight="600" color={primary}>
{expandedLibrary ? t('events.tasks.hideLibrary', 'Hide library') : t('events.tasks.viewAllLibrary', 'View all')}
</Text>
</Pressable>
{library.length === 0 ? (
<Text fontSize={12} fontWeight="500" color={subtle}>
{t('events.tasks.libraryEmpty', 'Keine weiteren Fotoaufgaben verfügbar.')}
</Text>
) : (
<YGroup {...({ borderWidth: 1, borderColor: border, borderRadius: "$4", overflow: "hidden" } as any)}>
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
<YGroup.Item key={`lib-${task.id}`}>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{task.title}
</Text>
}
subTitle={
task.description ? (
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{task.description}
</Text>
) : null
}
iconAfter={
<XStack space="$1.5" alignItems="center">
<Pressable onPress={() => quickAssign(task.id)}>
<XStack alignItems="center" space="$1">
<Plus size={14} color={primary} />
<Text fontSize={12} fontWeight="600" color={primary}>
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
</Text>
</XStack>
</Pressable>
<ChevronRight size={14} color={subtle} />
</XStack>
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
</YGroup.Item>
))}
</YGroup>
)}
{taskPanel}
</YStack>
)}