Compact tasks hero and harden sticky toolbar
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 10:59:58 +01:00
parent 7a71efedd1
commit a916bf8c4d
3 changed files with 86 additions and 42 deletions

View File

@@ -551,6 +551,12 @@ html.guest-theme.dark {
animation: admin-fade-up 220ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.admin-sticky-toolbar {
position: sticky;
top: calc(env(safe-area-inset-top, 0px) + 76px);
z-index: 45;
}
@media (prefers-reduced-motion: reduce) {
.admin-fade-up {
animation: none;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight, Check } from 'lucide-react';
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight, Check, Info } from 'lucide-react';
import { Card } from '@tamagui/card';
import { YStack, XStack } from '@tamagui/stacks';
import { YGroup } from '@tamagui/group';
@@ -179,6 +179,7 @@ export default function MobileEventTasksPage() {
const [emotionForm, setEmotionForm] = React.useState({ name: '', color: String(border) });
const [savingEmotion, setSavingEmotion] = React.useState(false);
const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false);
const [showTaskDetails, setShowTaskDetails] = React.useState(false);
const [quickNavSelection, setQuickNavSelection] = React.useState<TaskSectionKey | ''>('');
const [eventRecord, setEventRecord] = React.useState<TenantEvent | null>(null);
const [tasksToggleBusy, setTasksToggleBusy] = React.useState(false);
@@ -645,44 +646,67 @@ export default function MobileEventTasksPage() {
) : null}
{!loading ? (
<MobileCard space="$3">
<YStack space="$1">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('events.tasks.toggle.title', 'Photo tasks for this event')}
</Text>
<Text fontSize="$xs" color={muted}>
{t(
'events.tasks.toggle.description',
'Give guests optional photo tasks and prompts.'
)}
</Text>
</YStack>
<XStack alignItems="center" justifyContent="space-between" space="$3" flexWrap="wrap">
<PillBadge tone={tasksEnabled ? 'success' : 'warning'}>
{tasksEnabled
? t('events.tasks.toggle.active', 'ACTIVE')
: t('events.tasks.toggle.inactive', 'INACTIVE')}
</PillBadge>
<MobileCard space="$2" padding="$3">
<XStack alignItems="center" justifyContent="space-between" space="$2" flexWrap="wrap">
<YStack space="$1" flex={1} minWidth={180}>
<XStack alignItems="center" space="$2" flexWrap="wrap">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('events.tasks.toggle.title', 'Photo tasks for this event')}
</Text>
<PillBadge tone={tasksEnabled ? 'success' : 'warning'}>
{tasksEnabled
? t('events.tasks.toggle.active', 'ACTIVE')
: t('events.tasks.toggle.inactive', 'INACTIVE')}
</PillBadge>
</XStack>
{showTaskDetails ? (
<Text fontSize="$xs" color={muted}>
{t(
'events.tasks.toggle.description',
'Give guests optional photo tasks and prompts.'
)}
</Text>
) : null}
</YStack>
<XStack alignItems="center" space="$2">
<Pressable
onPress={() => setShowTaskDetails((prev) => !prev)}
aria-label={t(
'events.tasks.toggle.description',
'Give guests optional photo tasks and prompts.'
)}
>
<XStack
width={32}
height={32}
borderRadius={10}
alignItems="center"
justifyContent="center"
borderWidth={1}
borderColor={showTaskDetails ? withAlpha(primary, 0.45) : border}
backgroundColor={showTaskDetails ? withAlpha(primary, 0.12) : surfaceMuted}
>
<Info size={14} color={showTaskDetails ? primary : muted} />
</XStack>
</Pressable>
<Switch
size="$4"
checked={tasksEnabled}
onCheckedChange={handleTasksToggle}
aria-label={t('events.tasks.toggle.switchLabel', 'Photo tasks enabled')}
disabled={!canManageTasks || tasksToggleBusy}
>
<Switch.Thumb />
</Switch>
</XStack>
</XStack>
{showTaskDetails ? (
<Text fontSize="$xs" color={muted}>
{tasksEnabled
? t('events.tasks.toggle.onLabel', 'Guests see photo tasks')
: t('events.tasks.toggle.offLabel', 'Guest app shows photos only')}
</Text>
</XStack>
<XStack alignItems="center" justifyContent="space-between" marginTop="$2">
<Text fontSize="$xs" color={text} fontWeight="600">
{t('events.tasks.toggle.switchLabel', 'Photo tasks enabled')}
</Text>
<Switch
size="$4"
checked={tasksEnabled}
onCheckedChange={handleTasksToggle}
aria-label={t('events.tasks.toggle.switchLabel', 'Photo tasks enabled')}
disabled={!canManageTasks || tasksToggleBusy}
>
<Switch.Thumb />
</Switch>
</XStack>
) : null}
{isMember && !canManageTasks ? (
<Text fontSize="$xs" color={muted}>
{t('events.tasks.toggle.permissionHint', 'You do not have permission to change photo tasks.')}
@@ -750,12 +774,7 @@ export default function MobileEventTasksPage() {
</YStack>
</Card>
<YStack
position="sticky"
zIndex={45}
width="100%"
style={{ top: 'calc(env(safe-area-inset-top, 0px) + 76px)' }}
>
<YStack className="admin-sticky-toolbar" width="100%">
<Card
borderRadius={20}
borderWidth={2}

View File

@@ -156,8 +156,16 @@ vi.mock('@tamagui/list-item', () => ({
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress}>
Pressable: ({
children,
onPress,
...rest
}: {
children: React.ReactNode;
onPress?: () => void;
[key: string]: unknown;
}) => (
<button type="button" onClick={onPress} {...rest}>
{children}
</button>
),
@@ -278,6 +286,17 @@ describe('MobileEventTasksPage', () => {
);
});
it('toggles task details in the hero section', async () => {
render(<MobileEventTasksPage />);
const detailsLabel = 'Give guests optional photo tasks and prompts.';
expect(await screen.findByText('Photo tasks for this event')).toBeInTheDocument();
expect(screen.queryByText(detailsLabel)).not.toBeInTheDocument();
fireEvent.click(screen.getByLabelText(detailsLabel));
expect(screen.getByText(detailsLabel)).toBeInTheDocument();
});
it('enters selection mode on long press', async () => {
render(<MobileEventTasksPage />);