mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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:
1 parent
9036e71516
commit
b25683404e
8 files changed
+58
-43
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user