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>
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user