feat(uat-batch-3): wave-1 primitives — DatePicker, DateTimePicker, FileInputButton, ColumnPicker hideAll

Builds the foundational primitives that subsequent waves depend on.
None of these introduce new deps — date-fns, react-day-picker, and
shadcn Calendar were already in the tree.

- `<DatePicker>` and `<DateTimePicker>` in src/components/ui — desktop
  popover wrapping the existing shadcn Calendar (caption-dropdown nav
  so reps can jump months/years for the SkipAheadBanner backfill UX),
  mobile native input via useIsMobile. Drop-in for `<Input type=date>`
  / `<Input type=datetime-local>`.
- `<FileInputButton>` in src/components/ui — styled Button + hidden
  input, replaces browser-default file picker UI. Most queued sweep
  sites already used the hidden-input + Button-trigger pattern; the
  primitive lands for any new caller plus consistent filename display
  + clear button.
- ColumnPicker `hideAll()` footer item — symmetric to existing
  `showAll()`, with the same visibility gate. Lands platform-wide via
  the shared component.
- Migrated highest-leverage call sites to the new primitives:
  * MilestoneAdvanceButton (backfill UX)
  * Reminder form (datetime-local → DateTimePicker)
  * Snooze dialog (datetime-local → DateTimePicker)
  * External-EOI upload dialog (date + file picker)
  * Payments section (received-on date)
- Remaining 15+ date-input call sites parked for a follow-up sweep —
  several use react-hook-form `register` patterns that need careful
  migration to the new controlled-value contract.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 17:10:02 +02:00
parent 69444878ab
commit 8f42940c52
9 changed files with 443 additions and 40 deletions

View File

@@ -59,11 +59,18 @@ export function ColumnPicker({
onChange([]);
}
function hideAll() {
onChange(columns.filter((c) => !c.alwaysVisible).map((c) => c.id));
}
// The "All visible" affordance is only useful when something is
// hidden — a no-op button is noise.
const canShowAll = hidden.some((id) =>
columns.some((col) => col.id === id && !col.alwaysVisible),
);
// Mirror: "Hide all" is only useful when at least one toggleable
// column is currently visible.
const canHideAll = columns.some((col) => !col.alwaysVisible && !hiddenSet.has(col.id));
return (
<DropdownMenu>
@@ -113,13 +120,16 @@ export function ColumnPicker({
</DropdownMenuItem>
);
})}
{(canShowAll || canHideAll) && <DropdownMenuSeparator />}
{canShowAll && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={showAll} className="text-xs text-muted-foreground">
Show all columns
</DropdownMenuItem>
</>
<DropdownMenuItem onClick={showAll} className="text-xs text-muted-foreground">
Show all columns
</DropdownMenuItem>
)}
{canHideAll && (
<DropdownMenuItem onClick={hideAll} className="text-xs text-muted-foreground">
Hide all columns
</DropdownMenuItem>
)}
{onSaveView && (
<>