From 6b053e2a157ec68d900e81703d5140ba0499f150 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 17 Jul 2026 22:12:19 +0800 Subject: [PATCH] feat: replace native birth date input --- frontend/DESIGN.md | 8 +++ frontend/src/components/birth-date-picker.tsx | 72 +++++++++++++++++++ frontend/src/components/birth-time-intake.tsx | 16 ++--- .../tests/birth-date-picker-contract.test.ts | 15 ++++ 4 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/birth-date-picker.tsx diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 6746ecc9..4bc56e0b 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -102,6 +102,14 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: - **Accessibility:** native radio inputs remain focusable, every conditional field has a persistent label, status text uses live regions, and the complete flow is keyboard operable. - **Motion:** source-dependent fields enter with the existing 180ms opacity/vertical reveal; reduced-motion removes the translation. +### Birth date picker + +- **Composition:** shadcn outline Button trigger, Base UI Popover, and a single-select React DayPicker Calendar. +- **Range:** local dates from 1900-01-01 through today; future dates are disabled. Month and year dropdowns provide direct navigation, with newest years first. +- **Value:** display Chinese long dates while emitting the existing `YYYY-MM-DD` profile value without UTC conversion. +- **States:** empty, open, selected, focus-visible, disabled confirmed profile, and unavailable date. +- **Accessibility:** visible label, explicit trigger naming, 44px targets, keyboard calendar navigation, focus return, and collision-safe popup positioning. + ### Model selector - **Structure:** a compact text trigger sits below the composer and opens an upward popover aligned to its left edge. The trigger shows only the active model name; each option shows only its model name and radio selection state. diff --git a/frontend/src/components/birth-date-picker.tsx b/frontend/src/components/birth-date-picker.tsx new file mode 100644 index 00000000..f9514d2d --- /dev/null +++ b/frontend/src/components/birth-date-picker.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { format } from "date-fns"; +import { zhCN } from "date-fns/locale"; +import { CalendarIcon } from "lucide-react"; +import { useId, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { formatBirthDate, parseBirthDate } from "@/lib/birth-time-intake-model"; + +type BirthDatePickerProps = { + readonly value: string; + readonly disabled: boolean; + readonly onChange: (value: string) => void; +}; + +export function BirthDatePicker({ value, disabled, onChange }: BirthDatePickerProps) { + const labelId = useId(); + const valueId = useId(); + const [open, setOpen] = useState(false); + const selected = parseBirthDate(value); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + return ( +
+ 出生日期 + + } + > + + + { + if (nextDate === undefined) return; + onChange(formatBirthDate(nextDate)); + setOpen(false); + }} + /> + + +
+ ); +} diff --git a/frontend/src/components/birth-time-intake.tsx b/frontend/src/components/birth-time-intake.tsx index b54e870d..d52d0843 100644 --- a/frontend/src/components/birth-time-intake.tsx +++ b/frontend/src/components/birth-time-intake.tsx @@ -1,6 +1,7 @@ "use client"; import { useId } from "react"; +import { BirthDatePicker } from "@/components/birth-date-picker"; import { birthTimePeriodOptions, birthTimeSourceOptions, @@ -58,16 +59,11 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps) return (
- + onPatch({ date })} + /> {source === "legacy_import" && (

diff --git a/frontend/tests/birth-date-picker-contract.test.ts b/frontend/tests/birth-date-picker-contract.test.ts index 9e7ffda0..2f3347fd 100644 --- a/frontend/tests/birth-date-picker-contract.test.ts +++ b/frontend/tests/birth-date-picker-contract.test.ts @@ -3,9 +3,24 @@ import { existsSync, readFileSync } from "node:fs" import test from "node:test" const packageJson = readFileSync(new URL("../package.json", import.meta.url), "utf8") +const intake = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8") +const pickerUrl = new URL("../src/components/birth-date-picker.tsx", import.meta.url) test("provides the shadcn calendar and popover primitives", () => { assert.equal(existsSync(new URL("../src/components/ui/calendar.tsx", import.meta.url)), true) assert.equal(existsSync(new URL("../src/components/ui/popover.tsx", import.meta.url)), true) assert.match(packageJson, /"react-day-picker"/) }) + +test("replaces the native birth date input with the shadcn date picker", () => { + assert.equal(existsSync(pickerUrl), true) + assert.doesNotMatch(intake, /type="date"/) + assert.match(intake, /