Files
DSMMG/packages/react/src/Form.tsx
T
Dinawo 62317f2ad7
Release / Release / open changeset PR (push) Has been cancelled
CI / Build, typecheck, test, a11y (push) Has been cancelled
chore: initial DSMMG v0.2 — refonte architecturale complète
Mise en place du Design System ManageMate Group v0.2 — refonte du
système de tokens (préfixe --mmg-color-*), 9 presets accent
user-themable validés WCAG AA, overlays Radix UI + Floating UI,
Storybook 8 + Vitest + axe-core en CI, doc Astro Starlight,
DESIGN.md (format google-labs-code) et exports tokens DTCG/CSS/
TS/Figma/Tailwind v3 et v4.

- 4 packages monorepo pnpm : @managemate/{tokens,css,react,icons}
- 62 composants React headless-first (Sheet, HoverCard, ContextMenu,
  Slider, ToggleGroup, AvatarGroup, UserCard, ProfileHeader,
  MetricCard, PricingCard, FeatureCard, Text/Display/Eyebrow/Lead…)
- Lint contraste WCAG : 37/37 paires AA, branché CI
- Toast pile Sonner-style avec ResizeObserver
- Theming user (9 presets) sans casser sémantique fixe
- Identité Synapse (rose #D12B6A) préservée

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:08:38 +02:00

253 lines
7.3 KiB
TypeScript

import {
forwardRef,
useId,
type InputHTMLAttributes,
type SelectHTMLAttributes,
type TextareaHTMLAttributes,
type ReactNode,
} from "react";
import { cx } from "./utils";
import { Icon, type IconName } from "./Icon";
type FieldRenderArg = {
/** id du contrôle (à passer à <input>/<textarea>/<select>) */
id: string;
/** id du message à passer en aria-describedby (peut être undefined) */
describedBy: string | undefined;
/** L'état d'invalidité, à passer en aria-invalid */
invalid: boolean | undefined;
};
type FieldProps = {
label?: ReactNode;
hint?: ReactNode;
error?: ReactNode;
success?: ReactNode;
required?: boolean;
className?: string;
id?: string;
children: (arg: FieldRenderArg) => ReactNode;
};
export function Field({
label,
hint,
error,
success,
required,
className,
id: idProp,
children,
}: FieldProps) {
const auto = useId();
const id = idProp ?? auto;
const messageId = `${id}-message`;
const hasMessage = Boolean(error || success || hint);
const describedBy = hasMessage ? messageId : undefined;
const state = error ? "error" : success ? "success" : undefined;
return (
<div className={cx("mmg-field", state && `mmg-field--${state}`, className)}>
{label && (
<label
className={cx(
"mmg-field__label",
required && "mmg-field__label--required",
)}
htmlFor={id}
>
{label}
</label>
)}
{children({ id, describedBy, invalid: error ? true : undefined })}
{error ? (
<span id={messageId} className="mmg-field__error" role="alert">
{error}
</span>
) : success ? (
<span id={messageId} className="mmg-field__success">
{success}
</span>
) : hint ? (
<span id={messageId} className="mmg-field__hint">
{hint}
</span>
) : null}
</div>
);
}
/* — Input ————————————————————————————— */
export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
label?: ReactNode;
hint?: ReactNode;
error?: ReactNode;
success?: ReactNode;
size?: "sm" | "md" | "lg";
prefixIcon?: IconName;
suffixIcon?: IconName;
};
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ label, hint, error, success, size = "md", prefixIcon, suffixIcon, required, className, ...rest },
ref,
) {
const cls = cx(
"mmg-input",
size !== "md" && `mmg-input--${size}`,
className,
);
return (
<Field label={label} hint={hint} error={error} success={success} required={required}>
{({ id, describedBy, invalid }) =>
prefixIcon || suffixIcon ? (
<span className={cx("mmg-input-wrap", suffixIcon && "mmg-input-wrap--with-suffix")}>
{prefixIcon && (
<span className="mmg-input-wrap__icon"><Icon name={prefixIcon} /></span>
)}
<input
ref={ref}
id={id}
className={cls}
required={required}
aria-describedby={describedBy}
aria-invalid={invalid}
{...rest}
/>
{suffixIcon && (
<span className="mmg-input-wrap__icon mmg-input-wrap__icon--suffix">
<Icon name={suffixIcon} />
</span>
)}
</span>
) : (
<input
ref={ref}
id={id}
className={cls}
required={required}
aria-describedby={describedBy}
aria-invalid={invalid}
{...rest}
/>
)
}
</Field>
);
});
/* — Textarea ————————————————————————————— */
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
label?: ReactNode;
hint?: ReactNode;
error?: ReactNode;
success?: ReactNode;
fieldSize?: "sm" | "md" | "lg";
};
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
function Textarea(
{ label, hint, error, success, fieldSize = "md", required, className, ...rest },
ref,
) {
const cls = cx(
"mmg-textarea",
fieldSize !== "md" && `mmg-textarea--${fieldSize}`,
className,
);
return (
<Field label={label} hint={hint} error={error} success={success} required={required}>
{({ id, describedBy, invalid }) => (
<textarea
ref={ref}
id={id}
className={cls}
required={required}
aria-describedby={describedBy}
aria-invalid={invalid}
{...rest}
/>
)}
</Field>
);
},
);
/* — Select ————————————————————————————— */
export type SelectOption = { value: string; label: string; disabled?: boolean };
export type SelectProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> & {
label?: ReactNode;
hint?: ReactNode;
error?: ReactNode;
success?: ReactNode;
size?: "sm" | "md" | "lg";
options: SelectOption[];
placeholder?: string;
};
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
{ label, hint, error, success, size = "md", options, placeholder, required, className, ...rest },
ref,
) {
const cls = cx("mmg-select", size !== "md" && `mmg-select--${size}`, className);
return (
<Field label={label} hint={hint} error={error} success={success} required={required}>
{({ id, describedBy, invalid }) => (
<select
ref={ref}
id={id}
className={cls}
required={required}
aria-describedby={describedBy}
aria-invalid={invalid}
{...rest}
>
{placeholder && <option value="">{placeholder}</option>}
{options.map((o) => (
<option key={o.value} value={o.value} disabled={o.disabled}>
{o.label}
</option>
))}
</select>
)}
</Field>
);
});
/* — Checkbox / Radio ————————————————————————————— */
type CheckProps = InputHTMLAttributes<HTMLInputElement> & {
label: ReactNode;
};
export const Checkbox = forwardRef<HTMLInputElement, CheckProps>(function Checkbox(
{ label, className, ...rest },
ref,
) {
return (
<label className={cx("mmg-check", className)}>
<input ref={ref} type="checkbox" className="mmg-check__input" {...rest} />
<span className="mmg-check__label">{label}</span>
</label>
);
});
export const Radio = forwardRef<HTMLInputElement, CheckProps>(function Radio(
{ label, className, ...rest },
ref,
) {
return (
<label className={cx("mmg-check", className)}>
<input ref={ref} type="radio" className="mmg-check__input" {...rest} />
<span className="mmg-check__label">{label}</span>
</label>
);
});
/* — Switch ————————————————————————————— */
export const Switch = forwardRef<HTMLInputElement, CheckProps>(function Switch(
{ label, className, ...rest },
ref,
) {
return (
<label className={cx("mmg-switch", className)}>
<input ref={ref} type="checkbox" role="switch" className="mmg-switch__input" {...rest} />
<span className="mmg-check__label">{label}</span>
</label>
);
});