feat(todo): To-Do in the look of the packing list, detail pane always in reach

Rows get the packing list's filled checkbox, priority, due date and list
as quiet badges, and the assignee as an avatar with the name as its
tooltip. The tasks sit in a card whose head names the filter and holds
the sort, Priority or Due date, which moved out of the sidebar; a second
click on the active one goes back to the manual order. The progress card
matches the packing list's.

The detail pane is a card with a box in its head that ticks the task
off, and Delete and Save changes stay in sight: the To-Do view now fills
the space under the Lists bar instead of a fixed height that pushed the
pane's foot off screen. The detail pane and the new-task form share one
set of fields, and a new list is named in a dialog, from the sidebar
and from the form; the dialog moved to shared/NameDialog for both lists.

The To-Do help pictures are re-rendered, the texts that described the
old sidebar and form follow in all 27 languages, and the wiki page is
updated. The phone screens are unchanged.
This commit is contained in:
Maurice
2026-09-25 13:29:06 +02:00
parent dc60f39554
commit 1f79776070
51 changed files with 654 additions and 713 deletions
@@ -2,7 +2,7 @@ import {
CheckCheck, Luggage, Package, FolderPlus, Download, Trash2,
} from 'lucide-react'
import type { PackingState } from './usePackingListPanel'
import PackingNameDialog from './PackingNameDialog'
import NameDialog from '../shared/NameDialog'
export function PackingHeader(S: PackingState) {
const {
@@ -199,7 +199,7 @@ export function PackingHeader(S: PackingState) {
{/* Naming a new list or a template happens in a dialog of its own, not in a
field pushed into the page. */}
<PackingNameDialog
<NameDialog
open={canEdit && addingCategory}
title={t('packing.addCategory')}
placeholder={t('packing.newCategoryPlaceholder')}
@@ -209,7 +209,7 @@ export function PackingHeader(S: PackingState) {
onConfirm={handleAddNewCategory}
onClose={() => { setAddingCategory(false); setNewCatName('') }}
/>
<PackingNameDialog
<NameDialog
open={canEdit && isAdmin && items.length > 0 && showSaveTemplate}
title={t('packing.saveAsTemplate')}
placeholder={t('packing.templateName')}
@@ -1,47 +0,0 @@
// FE-PACKNAME-001 to FE-PACKNAME-004
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '../../../tests/helpers/render'
import PackingNameDialog from './PackingNameDialog'
function setup(value: string, open = true) {
const onChange = vi.fn()
const onConfirm = vi.fn()
const onClose = vi.fn()
render(
<PackingNameDialog open={open} title="Add list" placeholder="List name" confirmLabel="Add"
value={value} onChange={onChange} onConfirm={onConfirm} onClose={onClose} />,
)
return { onChange, onConfirm, onClose }
}
describe('PackingNameDialog', () => {
it('FE-PACKNAME-001: asks for the name under its title, confirm waiting for one', () => {
const { onChange } = setup('')
expect(screen.getByText('Add list')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled()
fireEvent.change(screen.getByPlaceholderText('List name'), { target: { value: 'Rain kit' } })
expect(onChange).toHaveBeenCalledWith('Rain kit')
})
it('FE-PACKNAME-002: Enter and the button confirm a typed name', () => {
const { onConfirm } = setup('Rain kit')
fireEvent.keyDown(screen.getByPlaceholderText('List name'), { key: 'Enter' })
fireEvent.click(screen.getByRole('button', { name: 'Add' }))
expect(onConfirm).toHaveBeenCalledTimes(2)
})
it('FE-PACKNAME-003: Enter on a blank name does nothing, Cancel closes', () => {
const { onConfirm, onClose } = setup(' ')
fireEvent.keyDown(screen.getByPlaceholderText('List name'), { key: 'Enter' })
expect(onConfirm).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(onClose).toHaveBeenCalledTimes(1)
})
it('FE-PACKNAME-004: stays out of the page while closed', () => {
setup('', false)
expect(screen.queryByPlaceholderText('List name')).toBeNull()
})
})
@@ -1,53 +0,0 @@
import Modal from '../shared/Modal'
import { useTranslation } from '../../i18n'
interface PackingNameDialogProps {
open: boolean
title: string
placeholder: string
confirmLabel: string
value: string
onChange: (value: string) => void
onConfirm: () => void
onClose: () => void
}
/**
* The small dialog that asks for one name: a new list, a template to save.
* It takes the name in the middle of the screen instead of pushing a field
* into the page; Enter confirms and Esc (from the modal) cancels.
*/
export default function PackingNameDialog({ open, title, placeholder, confirmLabel, value, onChange, onConfirm, onClose }: PackingNameDialogProps) {
const { t } = useTranslation()
const ready = value.trim().length > 0
return (
<Modal
isOpen={open}
onClose={onClose}
title={title}
size="sm"
footer={(
<div className="flex justify-end gap-2">
<button type="button" onClick={onClose} className="rounded-lg border border-edge px-4 py-2 text-body text-content-muted hover:bg-surface-hover">
{t('common.cancel')}
</button>
<button type="button" onClick={onConfirm} disabled={!ready}
className="rounded-lg bg-accent px-4 py-2 text-body font-medium text-accent-text hover:bg-accent-hover disabled:cursor-default disabled:opacity-40">
{confirmLabel}
</button>
</div>
)}
>
<input
autoFocus
type="text"
value={value}
onChange={e => onChange(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && ready) onConfirm() }}
placeholder={placeholder}
aria-label={title}
className="w-full rounded-lg border border-edge bg-surface-input px-3 py-2.5 text-body text-content outline-none focus:border-content-muted"
/>
</Modal>
)
}