# Task Form Dialog — keyboard + header polish — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Port `AddEventDialog`'s header style + full keyboard navigation + date-time keyboard entry into `TaskFormDialog`, reusing existing `DateField` and adding two small shared hooks. **Architecture:** Two new generic hooks (`useRovingFocus`, `useListboxKeys`) cover keyboard plumbing for any pill row + list popover. `parseDate` gains an optional trailing `HH:MM`. `DateField` gains `withTime` + `flat` props so it can be embedded inside another Popover without nesting. `TaskFormDialog` consumes all of these and drops its bespoke Due Popover + the footer ⌘+Enter hint. **Tech Stack:** React 19, TypeScript, shadcn/ui (`Popover`, `Calendar`, `Select`), react-day-picker, `react-i18next`, `TZDate` from `react-day-picker` (timezone-correct date construction), Drizzle ORM (unchanged), Electron. **Spec:** [docs/2026-05-14-task-form-dialog-kbd-design.md](./2026-05-14-task-form-dialog-kbd-design.md) **Mockup:** [docs/mockups/2026-05-14-task-form-dialog-mockup.html](./mockups/2026-05-14-task-form-dialog-mockup.html) **No test suite in the project (`adiuvAI/CLAUDE.md`: "No test suite currently.").** Verification per task is `npm run lint` + manual smoke. Plan does not include TDD steps; if pytest/vitest is introduced later, regression cases for `parseDate` belong in `docs/2026-05-14-task-form-dialog-kbd-design.md` § 3 table. **Commit cadence:** one commit per task. Each commit message follows the existing Conventional Commits style visible in `git log` (`feat:`, `refactor:`, `docs:`, …) and includes the `Co-Authored-By: Claude Opus 4.7 ` trailer. --- ## File Structure | File | Op | Responsibility | |---|---|---| | `adiuvAI/src/renderer/hooks/useRovingFocus.ts` | Create | Roving-tabindex hook for any horizontally-arranged group of focusable items (pills, tabs). | | `adiuvAI/src/renderer/hooks/useListboxKeys.ts` | Create | Listbox keyboard handler (↑/↓/Home/End/Enter/Esc/Tab) shared by all single- and multi-select popovers. | | `adiuvAI/src/renderer/lib/parseDate.ts` | Modify | Add optional trailing ` HH:MM` parsing. | | `adiuvAI/src/renderer/components/ui/date-field.tsx` | Modify | Add `withTime?: boolean` (renders HH/MM Selects in popover) and `flat?: boolean` (skips the internal Popover, renders Input + Calendar + Time row inline). | | `adiuvAI/src/renderer/components/tasks/TaskFormDialog.tsx` | Modify | New header (Title + Description, no border); pills row uses `useRovingFocus`; each list popover uses `useListboxKeys`; Due popover content is ``; remove `⌘+Enter` footer hint. Pill triggers become real ` ), ); PropertyPill.displayName = 'PropertyPill'; ``` - [ ] **Step 5.2 — Update callers that wrap PropertyPill in ``** `TaskFormDialog.tsx` currently wraps each `` inside `` so `PopoverTrigger asChild` can attach a ref to a single element. Now that `PropertyPill` is itself a ` ); } if (it.kind === 'none') { return ( ); } return ( ); })} ); } ``` In the Project popover, replace the inline list JSX with `` and pass: ```tsx { setValues((v) => ({ ...v, projectId: id })); setProjectPopoverOpen(false); }} onCreate={() => setCreatingProject(true)} onClose={() => setProjectPopoverOpen(false)} /> ``` - [ ] **Step 6c.3 — Priority popover** Replace the existing three-button `priorities.map` with a small listbox wrapper: ```tsx function PriorityList({ value, onSelect, onClose, }: { value: string; onSelect: (v: 'high' | 'medium' | 'low') => void; onClose: () => void; }) { const { t } = useTranslation(); const items = ['high', 'medium', 'low'] as const; const initial = Math.max(0, items.indexOf(value as typeof items[number])); const listbox = useListboxKeys({ itemCount: items.length, initialIndex: initial, onSelect: (i) => onSelect(items[i]), onClose, }); useEffect(() => { listbox.focusIndex(listbox.activeIndex); /* eslint-disable-next-line */ }, []); return (
{items.map((p, i) => ( ))}
); } ``` Drop in the Priority ``: ```tsx setValues((v) => ({ ...v, priority: p }))} onClose={() => {/* shadcn closes when triggering element loses focus; explicit close below */}} /> ``` For consistency with Project, control the Priority popover's `open` state from the parent (`const [priorityOpen, setPriorityOpen] = useState(false);`), pass it to ``, and `onClose={() => setPriorityOpen(false)}`. On `onSelect`, also call `setPriorityOpen(false)`. - [ ] **Step 6c.4 — Status popover** Identical pattern to Priority. Items: `['todo', 'in_progress', 'done']`. Component `StatusList`. Parent state `statusOpen / setStatusOpen`. - [ ] **Step 6c.5 — Assignees popover** Multi-select. Build `AssigneesList` similar to the others but `onSelect` toggles membership in `values.assignees` (and the popover stays open): ```tsx function AssigneesList({ known, selected, onToggle, onClose, newName, onNewNameChange, onAddNew, }: { known: string[]; selected: string[]; onToggle: (name: string) => void; onClose: () => void; newName: string; onNewNameChange: (s: string) => void; onAddNew: () => void; }) { const { t } = useTranslation(); const listbox = useListboxKeys({ itemCount: known.length, initialIndex: 0, onSelect: (i) => onToggle(known[i]), onClose, }); useEffect(() => { if (known.length > 0) listbox.focusIndex(0); // eslint-disable-next-line }, []); return (
{known.map((name, i) => { const isOn = selected.includes(name); return ( ); })}
onNewNameChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); onAddNew(); } }} className="h-8 text-sm flex-1" />
); } ``` In the Assignees ``, replace the existing JSX with: ```tsx setValues((v) => ({ ...v, assignees: v.assignees.includes(name) ? v.assignees.filter((a) => a !== name) : [...v.assignees, name], })) } onClose={() => {/* uncontrolled Popover; native Esc/blur closes */}} newName={assigneeInput} onNewNameChange={setAssigneeInput} onAddNew={addNewAssignee} /> ``` (Assignees can stay uncontrolled — shadcn handles dismissal on outside click + Esc inside the listbox is already wired by `useListboxKeys` via `onClose`; for a true close-on-esc, mirror the controlled pattern used for Priority/Status if desired. Keeping uncontrolled is consistent with the current code.) ### 6d. Due popover via `DateField` - [ ] **Step 6d.1 — Replace the entire Due `` block** Remove the `HOURS`/`MINUTES` constants and the `derivePartsInTz` / `updateDueTime` helpers from `TaskFormDialog.tsx` — `DateField` now owns time handling. Then replace the Due `` body with: ```tsx } label={t('tasks.colDue')} value={values.dueDate ? formatDueDate(values.dueDate, prefs) : null} empty={!values.dueDate} aria-label={t('tasks.colDue')} /> setValues((v) => ({ ...v, dueDate: d ? d.getTime() : null }))} placeholder={t('tasks.colDue')} aria-label={t('tasks.colDue')} /> ``` Add `import { DateField } from '@/components/ui/date-field';` at the top and remove unused imports (`TZDate`, `Calendar`, `Select`* if no longer used elsewhere in the file — re-run lint after). **Note on timezones:** the old code constructed `TZDate` using user `prefs.timezone` so e.g. `00:00` was interpreted in the user's IANA zone. `DateField` uses plain `Date` (browser local time). In Electron this is the user's OS timezone, which is what `prefs.timezone` already defaults to. For users who manually override `prefs.timezone` to a non-OS zone, due times here will reflect OS-local instead. This matches `AddEventDialog`'s current behavior and is acceptable; if/when timezone-aware editing is reintroduced, both `DateField` and `AddEventDialog` should change together. ### 6e. Remove ⌘+Enter footer hint - [ ] **Step 6e.1 — Audit footer** Re-read the current footer: the existing footer has only Cancel + Submit + (optional Paperclip in edit mode). It does NOT contain a "⌘+Enter to create" hint — that was a mockup-only element. The form-level `onKeyDown` that submits on `⌘/Ctrl+Enter` stays. No code change required for 6e; this step is a checklist confirmation. Move on. ### 6f. Lint + smoke + commit - [ ] **Step 6f.1 — Lint** ``` npm run lint ``` Expected: clean. Address any "unused import" errors from removed `TZDate` / `Select` / `Calendar` etc. - [ ] **Step 6f.2 — Manual smoke (full keyboard flow)** `npm start`, then in the running app: 1. Open Tasks page → "New task". Header shows title + muted description, no separator line. 2. Tab from the title input → focus lands on the description textarea. Tab again → first pill (Project) shows focus ring. 3. Press ←/→ and ↑/↓: focus moves between pills. 4. With Project pill focused, press Enter: popover opens; "No project" or current selection is focused. 5. ↓/↑ navigate items. Enter selects → popover closes → focus returns to the Project pill. 6. Repeat for Priority, Status, Assignees. For Assignees, Enter toggles, Esc closes. 7. With Due pill focused, press Enter: popover opens with `DateField`. Type `30/04/2026 14:30`, press Enter — pill value reads `30/04/2026 14:30` (or US format if `FormatPrefs.dateFormat = 'MM/dd/yyyy'`). 8. Re-open Due, click a calendar day — date updates while keeping the time. Change hour Select → pill reflects new time. 9. `Ctrl+Enter` anywhere in the form submits. 10. Edit mode: re-open an existing task with a due date that has `HH:MM` and verify the field renders both date and time. - [ ] **Step 6f.3 — Commit** ``` git -C adiuvAI add src/renderer/components/tasks/TaskFormDialog.tsx git -C adiuvAI commit -m "feat(TaskFormDialog): new header, full keyboard nav, DateField-based due" ``` --- ## Task 7 — i18n keys in all five languages **Files:** - Modify: `adiuvAI/src/renderer/locales/{en,it,es,fr,de}/translation.json` **Why:** Header description string is new; missing keys would print the key path. - [ ] **Step 7.1 — Add keys under the `tasks` namespace** For each language, add (or merge into the existing `"tasks": { ... }` block) the two new keys: EN: ```json "newTaskDescription": "Capture what needs doing. Set properties below or refine later.", "editTaskDescription": "Update the task details, properties, and attachments." ``` IT: ```json "newTaskDescription": "Annota cosa va fatto. Imposta le proprietà sotto o aggiornale dopo.", "editTaskDescription": "Aggiorna dettagli, proprietà e allegati dell'attività." ``` ES: ```json "newTaskDescription": "Anota qué hay que hacer. Define las propiedades abajo o más tarde.", "editTaskDescription": "Actualiza los detalles, propiedades y adjuntos de la tarea." ``` FR: ```json "newTaskDescription": "Notez ce qu'il faut faire. Définissez les propriétés ci-dessous ou plus tard.", "editTaskDescription": "Mettez à jour les détails, propriétés et pièces jointes de la tâche." ``` DE: ```json "newTaskDescription": "Halten Sie fest, was zu tun ist. Eigenschaften unten oder später setzen.", "editTaskDescription": "Aktualisieren Sie Details, Eigenschaften und Anhänge der Aufgabe." ``` Place the keys near other `tasks.new*` / `tasks.edit*` entries (alphabetical inside `tasks` is the existing convention — match it). - [ ] **Step 7.2 — Lint** ``` npm run lint ``` Expected: clean (JSON files don't lint, but TypeScript references resolve). - [ ] **Step 7.3 — Smoke** `npm start`, switch language in Settings → General → Language and confirm the dialog description renders in the chosen language. - [ ] **Step 7.4 — Commit** ``` git -C adiuvAI add src/renderer/locales/en/translation.json src/renderer/locales/it/translation.json src/renderer/locales/es/translation.json src/renderer/locales/fr/translation.json src/renderer/locales/de/translation.json git -C adiuvAI commit -m "i18n: add tasks.newTaskDescription / editTaskDescription" ``` --- ## Task 8 — Bump submodule in the monorepo **Files:** - Modify: top-level `adiuvAI` submodule pointer. - [ ] **Step 8.1 — Stage submodule bump** From the monorepo root (`c:\Users\PC-Roby\Documents\_adiuvai_workspace`): ``` git add adiuvAI git status ``` Expected: `modified: adiuvAI (new commits)` is now staged. - [ ] **Step 8.2 — Commit at monorepo level** ``` git commit -m "feat: bump adiuvAI submodule — task form keyboard polish" ``` - [ ] **Step 8.3 — Run graphify update** Per project `CLAUDE.md`: ``` graphify update . ``` This keeps the knowledge graph aligned with the new hooks / components. AST-only, no API cost. - [ ] **Step 8.4 — Final smoke + done** Reopen the Task dialog one more time and run through the full keyboard flow in §6f.2 to confirm nothing regressed after the submodule pointer was bumped. --- ## Self-review (post-write, pre-handoff) - **Spec coverage:** - §1 Header → Task 6a. - §2 Pills row roving focus → Task 1 + 6b. List popovers → Task 2 + 6c. Calendar nav → react-day-picker built-in (no task needed; surfaced inside `CalendarTimeBody` in Task 4). Description Enter = newline → unchanged (Task 6e checklist). - §3 Date+time: - parseDate suffix → Task 3. - DateField `withTime` + `flat` → Task 4. - TaskFormDialog Due popover replacement → Task 6d. - §4 Files list → mirrored 1-for-1. - §5 Accessibility — `role="toolbar"` (6b.3), `role="listbox"` / `role="option"` / `aria-multiselectable` / `aria-selected` (6c). DateField a11y unchanged. - §6 Out-of-scope items are not in the plan. Good. - **Placeholder scan:** no "TBD", "TODO", or hand-waving steps. Each code-changing step shows code. The "no internal tests" note explicitly says manual smoke instead of inventing fake tests. - **Type consistency:** - `useRovingFocus` exposes `{ activeIndex, setActive, getItemProps }` — used in Task 6b. - `useListboxKeys` exposes `{ activeIndex, setActive, focusIndex, getItemProps }` — used in Tasks 6c.2–6c.5. - `DateField` new props `withTime`, `flat` — consumed in Task 6d. - `PropertyPill` becomes a forwardRef `