Files
Kordant/web/src/components/ui/Input.tsx
Michael Freno 4118a25388 feat: add UI primitive library — Button, Card, Input, Badge, Modal, Toast
- Add cn() utility for class merging in lib/utils.ts
- Button: primary/secondary/ghost/danger variants, sm/md/lg sizes, disabled/loading states
- Card: gradient-card background with optional header/footer slots
- Input: text/email/password/number types with label, error, helper text, focus ring
- Badge: default/success/warning/error/info variants
- Modal: Portal-based dialog with focus trap, ESC/backdrop close, animations
- Toast: ToastProvider context with show/dismiss/auto-dismiss and variant support
- Barrel export via index.ts
- 46 unit tests across all primitives
- Configure vitest with vite-plugin-solid for JSX support
2026-05-25 13:03:00 -04:00

67 lines
1.9 KiB
TypeScript

import { cn } from "~/lib/utils";
import type { JSX } from "solid-js";
interface InputProps {
label?: string;
type?: "text" | "email" | "password" | "number";
value?: string;
onInput?: (e: InputEvent & { currentTarget: HTMLInputElement }) => void;
error?: string;
helperText?: string;
placeholder?: string;
class?: string;
id?: string;
name?: string;
required?: boolean;
disabled?: boolean;
}
export default function Input(props: InputProps) {
const id = () =>
props.id ??
props.name ??
globalThis.crypto?.randomUUID?.() ??
Math.random().toString(36).slice(2, 10);
return (
<div class={cn("flex flex-col gap-1", props.class)}>
{props.label && (
<label
for={id()}
class="text-sm font-medium text-[var(--color-text-primary)]"
>
{props.label}
{props.required && (
<span class="text-[var(--color-error)] ml-1">*</span>
)}
</label>
)}
<input
id={id()}
type={props.type ?? "text"}
value={props.value ?? ""}
onInput={props.onInput}
placeholder={props.placeholder}
name={props.name}
disabled={props.disabled}
class={cn(
"w-full bg-transparent border rounded-lg px-4 py-2 text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] transition-all duration-200",
props.error
? "border-[var(--color-error)] focus:ring-[var(--color-error)]"
: "border-[var(--color-border)] focus:ring-[var(--color-focus-ring)]",
"focus:outline-none focus:ring-2",
props.disabled && "opacity-50 cursor-not-allowed",
)}
/>
{props.error && (
<p class="text-sm text-[var(--color-error)]">{props.error}</p>
)}
{props.helperText && !props.error && (
<p class="text-sm text-[var(--color-text-tertiary)]">
{props.helperText}
</p>
)}
</div>
);
}