From b25683404e94735a5e90436b72ce7353006a9544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 15 May 2026 18:27:14 +0200 Subject: [PATCH] refactor(ui): move PasswordInput into the shared UI layer with a 44 px toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move `` from `src/components/settings/password-input.tsx` to `src/components/ui/password-input.tsx` so the auth surfaces 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` --- src/components/admin/_shared.tsx | 4 +- src/components/settings/account-section.tsx | 2 +- src/components/settings/ai-section.tsx | 2 +- .../settings/integrations-section.tsx | 2 +- src/components/settings/ntfy-card.tsx | 2 +- src/components/settings/password-input.tsx | 36 ------------- src/components/settings/telegram-card.tsx | 2 +- src/components/ui/password-input.tsx | 51 +++++++++++++++++++ 8 files changed, 58 insertions(+), 43 deletions(-) delete mode 100644 src/components/settings/password-input.tsx create mode 100644 src/components/ui/password-input.tsx diff --git a/src/components/admin/_shared.tsx b/src/components/admin/_shared.tsx index 59c64b96b..b4ef25354 100644 --- a/src/components/admin/_shared.tsx +++ b/src/components/admin/_shared.tsx @@ -6,7 +6,7 @@ import { toast } from "sonner"; import { Cpu, Fingerprint, Globe, KeyRound } from "lucide-react"; import { Switch } from "@/components/ui/switch"; import { useTranslations } from "@/lib/i18n/context"; -import { PasswordInput as SettingsPasswordInput } from "@/components/settings/password-input"; +import { PasswordInput as SharedPasswordInput } from "@/components/ui/password-input"; /** * Re-export the canonical password input so admin sections have access to @@ -15,7 +15,7 @@ import { PasswordInput as SettingsPasswordInput } from "@/components/settings/pa * `button-name` violation on `/admin/integrations` and * `/admin/users` (axe-core, WCAG 2.1.2). */ -export const PasswordInput = SettingsPasswordInput; +export const PasswordInput = SharedPasswordInput; export interface AdminUser { id: string; diff --git a/src/components/settings/account-section.tsx b/src/components/settings/account-section.tsx index 5a37c8b16..615d863b6 100644 --- a/src/components/settings/account-section.tsx +++ b/src/components/settings/account-section.tsx @@ -37,7 +37,7 @@ import { import { Input } from "@/components/ui/input"; import { DateInput } from "@/components/ui/date-input"; import { Label } from "@/components/ui/label"; -import { PasswordInput } from "@/components/settings/password-input"; +import { PasswordInput } from "@/components/ui/password-input"; import { useAuth } from "@/hooks/use-auth"; import { formatDate } from "@/lib/format"; import { locales, localeLabels, type Locale } from "@/lib/i18n/config"; diff --git a/src/components/settings/ai-section.tsx b/src/components/settings/ai-section.tsx index 088f11b51..5dc57d6b9 100644 --- a/src/components/settings/ai-section.tsx +++ b/src/components/settings/ai-section.tsx @@ -32,7 +32,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { PasswordInput } from "@/components/settings/password-input"; +import { PasswordInput } from "@/components/ui/password-input"; import { useAuth } from "@/hooks/use-auth"; import { formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; diff --git a/src/components/settings/integrations-section.tsx b/src/components/settings/integrations-section.tsx index 83d63bf07..79a0f9c44 100644 --- a/src/components/settings/integrations-section.tsx +++ b/src/components/settings/integrations-section.tsx @@ -27,7 +27,7 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { PasswordInput } from "@/components/settings/password-input"; +import { PasswordInput } from "@/components/ui/password-input"; import { TestConnectionButton } from "@/components/settings/test-connection-button"; import { IntegrationStatusPill } from "@/components/settings/integration-status-pill"; import type { IntegrationPillState } from "@/components/settings/integration-status-pill"; diff --git a/src/components/settings/ntfy-card.tsx b/src/components/settings/ntfy-card.tsx index 5320d8fb8..afa9221bc 100644 --- a/src/components/settings/ntfy-card.tsx +++ b/src/components/settings/ntfy-card.tsx @@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { PasswordInput } from "@/components/settings/password-input"; +import { PasswordInput } from "@/components/ui/password-input"; import { useTranslations } from "@/lib/i18n/context"; interface NtfySettings { diff --git a/src/components/settings/password-input.tsx b/src/components/settings/password-input.tsx deleted file mode 100644 index 0c44a1040..000000000 --- a/src/components/settings/password-input.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Eye, EyeOff } from "lucide-react"; - -import { Input } from "@/components/ui/input"; -import { useTranslations } from "@/lib/i18n/context"; - -/** - * Password input with a built-in show/hide toggle. Extracted from the legacy - * settings monolith so every section that needs a masked secret (Withings, - * moodLog, ntfy, Telegram, AI provider keys, account password) can share the - * same UX without duplicating the visibility-toggle plumbing. - */ -export function PasswordInput(props: React.ComponentProps) { - const [visible, setVisible] = useState(false); - const { t } = useTranslations(); - return ( -
- - -
- ); -} diff --git a/src/components/settings/telegram-card.tsx b/src/components/settings/telegram-card.tsx index 2619e098d..28298fd2a 100644 --- a/src/components/settings/telegram-card.tsx +++ b/src/components/settings/telegram-card.tsx @@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { PasswordInput } from "@/components/settings/password-input"; +import { PasswordInput } from "@/components/ui/password-input"; import { useTranslations } from "@/lib/i18n/context"; interface TelegramSettings { diff --git a/src/components/ui/password-input.tsx b/src/components/ui/password-input.tsx new file mode 100644 index 000000000..7e1ac7625 --- /dev/null +++ b/src/components/ui/password-input.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; + +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; +import { useTranslations } from "@/lib/i18n/context"; + +/** + * Password input with a built-in show/hide toggle. Lives in the shared UI + * layer so every surface that needs a masked secret (Withings, moodLog, + * ntfy, Telegram, AI provider keys, account password, auth login / + * register) can share the same UX without duplicating the + * visibility-toggle plumbing. + * + * v1.4.27 R3d MB2 — the icon-only toggle wraps in an `inline-flex h-11 + * w-11` hit area so the 44 px WCAG 2.5.5 floor is honoured on touch. + * The icon stays at 16 px; the surrounding padding does the work. The + * input itself gets `pr-12` so user input never collides with the + * toggle glyph. + */ +export function PasswordInput({ + className, + ...props +}: React.ComponentProps) { + const [visible, setVisible] = useState(false); + const { t } = useTranslations(); + return ( +
+ + +
+ ); +}