feat(ui): lift the password toggle into a shared primitive

Move `<PasswordInput>` from `src/components/settings/password-input.tsx`
to `src/components/ui/password-input.tsx` so the auth surfaces (login
and register) and every settings card that masks a secret consume one
primitive. The two surfaces previously diverged on toggle markup and
aria-label wiring; the new home in `ui/` keeps them in lock-step.

While the primitive moves, lift the icon-only show/hide toggle to a
44 px hit area per WCAG 2.5.5 — `inline-flex h-11 w-11` wrapper with
the eye icon centred inside. The input itself gets `pr-12` so user
input never collides with the toggle glyph.

Consumer imports updated in:
- `src/components/settings/account-section.tsx`
- `src/components/settings/ai-section.tsx`
- `src/components/settings/integrations-section.tsx`
- `src/components/settings/ntfy-card.tsx`
- `src/components/settings/telegram-card.tsx`
- `src/components/admin/_shared.tsx`

The `<DialogContent>` close-X tap-target lift to `min-h-9 min-w-9`
landed in the parallel `ResponsiveSheet` commit because both buckets
share the same `dialog.tsx` file. See Decision I — 36 px is the
intentional exception for fixed icon-only close affordances.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 18:26:06 +02:00
1 parent 65fd0bffdf
commit bd7cb938c9
3 files changed
+48 -77

No files matched your search

+16 -24
View File
@@ -5,13 +5,7 @@ import { useAuth } from "@/hooks/use-auth";
import { MeasurementForm } from "@/components/measurements/measurement-form";
import { MeasurementList } from "@/components/measurements/measurement-list";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Plus, Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
@@ -48,25 +42,23 @@ export default function MeasurementsPage() {
{t("measurements.subtitle")}
</p>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
{t("measurements.addMeasurement")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("measurements.addMeasurement")}</DialogTitle>
</DialogHeader>
<MeasurementForm
onSuccess={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
/>
</DialogContent>
</Dialog>
<Button onClick={() => setDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
{t("measurements.addMeasurement")}
</Button>
</div>
<ResponsiveSheet
open={dialogOpen}
onOpenChange={setDialogOpen}
title={t("measurements.addMeasurement")}
>
<MeasurementForm
onSuccess={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
/>
</ResponsiveSheet>
<MeasurementList onAddFirst={() => setDialogOpen(true)} />
</div>
);
+16 -24
View File
@@ -5,13 +5,7 @@ import { useAuth } from "@/hooks/use-auth";
import { MoodForm } from "@/components/mood/mood-form";
import { MoodList } from "@/components/mood/mood-list";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Plus, Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
@@ -48,25 +42,23 @@ export default function MoodPage() {
{t("mood.subtitle")}
</p>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
{t("mood.addEntry")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("mood.addEntry")}</DialogTitle>
</DialogHeader>
<MoodForm
onSuccess={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
/>
</DialogContent>
</Dialog>
<Button onClick={() => setDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
{t("mood.addEntry")}
</Button>
</div>
<ResponsiveSheet
open={dialogOpen}
onOpenChange={setDialogOpen}
title={t("mood.addEntry")}
>
<MoodForm
onSuccess={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
/>
</ResponsiveSheet>
<MoodList onAddFirst={() => setDialogOpen(true)} />
</div>
);
+16 -29
View File
@@ -26,12 +26,7 @@ import type { DataSummary as DataSummaryType } from "@/lib/analytics/trends";
import dynamic from "next/dynamic";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
DropdownMenu,
DropdownMenuContent,
@@ -523,35 +518,27 @@ export default function DashboardPage() {
* line is free for established users. */}
<TourLauncher ready={data !== undefined} />
{/* Quick Entry Dialogs */}
<Dialog
{/* Quick Entry Sheets — bottom-sheet on `<md`, centred Dialog on `md+`. */}
<ResponsiveSheet
open={quickEntryDialog === "measurement"}
onOpenChange={(open) => !open && setQuickEntryDialog(null)}
title={t("measurements.addMeasurement")}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("measurements.addMeasurement")}</DialogTitle>
</DialogHeader>
<MeasurementForm
onSuccess={() => setQuickEntryDialog(null)}
onCancel={() => setQuickEntryDialog(null)}
/>
</DialogContent>
</Dialog>
<Dialog
<MeasurementForm
onSuccess={() => setQuickEntryDialog(null)}
onCancel={() => setQuickEntryDialog(null)}
/>
</ResponsiveSheet>
<ResponsiveSheet
open={quickEntryDialog === "mood"}
onOpenChange={(open) => !open && setQuickEntryDialog(null)}
title={t("mood.addEntry")}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("mood.addEntry")}</DialogTitle>
</DialogHeader>
<MoodForm
onSuccess={() => setQuickEntryDialog(null)}
onCancel={() => setQuickEntryDialog(null)}
/>
</DialogContent>
</Dialog>
<MoodForm
onSuccess={() => setQuickEntryDialog(null)}
onCancel={() => setQuickEntryDialog(null)}
/>
</ResponsiveSheet>
{(() => {
type TrendEntry = { id: string; order: number; node: React.ReactNode };