refactor(ui): move PasswordInput into the shared UI layer with a 44 px toggle

Move `<PasswordInput>` 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`
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 18:27:14 +02:00
1 parent 9036e71516
commit b25683404e
8 files changed
+58 -43

No files matched your search

+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
@@ -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";
+1 -1
View File
@@ -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 {
@@ -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<typeof Input>) {
const [visible, setVisible] = useState(false);
const { t } = useTranslations();
return (
<div className="relative">
<Input {...props} type={visible ? "text" : "password"} />
<button
type="button"
tabIndex={-1}
// WCAG 2.1.2 button-name — the icon-only toggle has no visible text
// and screen readers fall back to the icon's filename otherwise.
aria-label={
visible ? t("common.hidePassword") : t("common.showPassword")
}
onClick={() => setVisible((v) => !v)}
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-3 -translate-y-1/2"
>
{visible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
);
}
+1 -1
View File
@@ -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 {
+51
View File
@@ -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<typeof Input>) {
const [visible, setVisible] = useState(false);
const { t } = useTranslations();
return (
<div className="relative">
<Input
{...props}
type={visible ? "text" : "password"}
className={cn("pr-12", className)}
/>
<button
type="button"
tabIndex={-1}
// WCAG 2.1.2 button-name — the icon-only toggle has no visible text
// and screen readers fall back to the icon's filename otherwise.
aria-label={
visible ? t("common.hidePassword") : t("common.showPassword")
}
onClick={() => setVisible((v) => !v)}
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-1 inline-flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-md"
>
{visible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
);
}