feat(v1): bloquants release v1 — tests, stories, visual regression, gouvernance, publishing
6 chantiers v1 sur 7 livrés (DataTable refonte reportée car nécessite 2-3j en propre — TanStack Table + virtualisation + filter builder). v1-A — Tests (4 → 22 fichiers) : - Avatar, AvatarGroup, UserCard, MetricCard, ProfileHeader, Tooltip, Sheet, Drawer, Slider, ToggleGroup, Tabs, Pagination, Accordion, Switch, Badge, ConfirmDialog, Popover, Menu, Text, PricingCard, FeatureCard, Toast — chacun avec render + clavier + axe-core. v1-B — Storybook (7 → 23 fichiers) : - Avatar, UserCard, ProfileHeader, MetricCard, PricingCard, FeatureCard, Sheet (4 sides), HoverCard, Slider, ToggleGroup, Menu+ContextMenu, Toast (avec démo "Empiler 5"), Tabs, Pagination, Accordion, Badge. v1-D — Visual regression Playwright : - playwright.config.ts (light + dark, threshold strict 0.2) - e2e/visual.spec.ts (20 stories critiques) - Step CI + upload report en cas de fail v1-E — Site doc Starlight rempli : - 11 pages composants détaillées (Button, Input, Tooltip, Dialog, Toast, Avatar, ThemePicker, MetricCard, PricingCard, ToggleGroup, Slider) avec API, anatomie, do/don't, A11y. v1-F — Publishing Verdaccio : - verdaccio/config.yaml, docker-compose.verdaccio.yml, .npmrc - README setup local + déploiement prod + backups + sécurité v1-G — Gouvernance : - LICENSE, CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md - CODEOWNERS, PR template, 3 issue templates (bug/feature/rfc) Bug fix bonus : tooltip dark mode (text-primary comme bg + text-inverse comme texte → blanc-sur-blanc invisible). Remplacé par neutral-900/0 en light + bg-raised/text-primary en dark. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Avatar / AvatarGroup
|
||||
description: Photo de profil avec initiales fallback, status indicator, AvatarGroup empilé.
|
||||
---
|
||||
|
||||
## Avatar
|
||||
|
||||
```tsx
|
||||
import { Avatar } from "@managemate/react";
|
||||
|
||||
<Avatar src="/photo.png" alt="Marie Dupont" size="lg" status="online" />
|
||||
<Avatar initials="MD" alt="Marie Dupont" size="lg" />
|
||||
```
|
||||
|
||||
| Prop | Type | Défaut |
|
||||
|---|---|---|
|
||||
| `src` | `string` | — |
|
||||
| `initials` | `string` | — |
|
||||
| `alt` | `string` | — (toujours fournir pour a11y) |
|
||||
| `size` | `"xs"|"sm"|"md"|"lg"|"xl"|"2xl"` | `"md"` |
|
||||
| `shape` | `"circle"|"square"` | `"circle"` |
|
||||
| `status` | `"online"|"away"|"busy"|"offline"` | — |
|
||||
| `color` | `"auto"|"neutral"|"brand"|"blue"|...` | `"auto"` |
|
||||
| `bordered` | `boolean` | `false` |
|
||||
|
||||
### Couleur auto
|
||||
|
||||
Si `initials` est fourni sans `src`, une couleur est générée déterministiquement depuis les initiales (palette `brand / blue / green / amber / violet`). Stable entre renders pour le même nom.
|
||||
|
||||
### Status indicator
|
||||
|
||||
Petit point en bas-droite avec `aria-label` :
|
||||
- `online` → vert "En ligne"
|
||||
- `away` → ambre "Absent"
|
||||
- `busy` → rouge "Occupé"
|
||||
- `offline` → gris "Hors ligne"
|
||||
|
||||
## AvatarGroup
|
||||
|
||||
```tsx
|
||||
<AvatarGroup
|
||||
max={4}
|
||||
total={12} // affiche +8 même si on passe seulement 4 avatars
|
||||
size="md"
|
||||
avatars={[
|
||||
{ initials: "MD", alt: "Marie" },
|
||||
{ initials: "JM", alt: "Jean" },
|
||||
{ initials: "SB", alt: "Sophie" },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
Empilage avec overlap négatif. Hover sur un avatar le remonte en z-index pour identifier qui c'est.
|
||||
|
||||
## Do / Don't
|
||||
|
||||
✅ **Do**
|
||||
- Toujours fournir `alt` (nom complet), même avec image.
|
||||
- Préférer `initials` à `src` quand l'image est absente — pas d'icône générique.
|
||||
- Utiliser `bordered` quand l'avatar est sur un fond chargé (Hero, AvatarGroup).
|
||||
|
||||
❌ **Don't**
|
||||
- Pas d'avatar sans `alt` (RGAA 1.1).
|
||||
- Pas d'image inline sans `loading="lazy"` (déjà géré).
|
||||
- Pas de status `online` sans rafraîchissement temps réel — sinon mensonger.
|
||||
|
||||
## A11y
|
||||
|
||||
- `role="img"` + `aria-label` quand pas d'image (juste initiales).
|
||||
- `<img alt>` sinon.
|
||||
- Status indicator a son propre `aria-label` (annoncé séparément).
|
||||
- Couleur auto-générée passe AA (background `*-100`, color `*-800`).
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Dialog / Sheet / Drawer
|
||||
description: Overlays modaux — Radix Dialog avec focus trap, scroll lock, restitution focus.
|
||||
---
|
||||
|
||||
## Quel composant choisir ?
|
||||
|
||||
| Composant | Usage | Animation |
|
||||
|---|---|---|
|
||||
| `<Dialog>` | Centré, focus sur une action courte. Confirmation, formulaire compact. | Fade + scale |
|
||||
| `<Sheet>` | Glisse depuis un bord (4 sides × 5 sizes). Édition longue, navigation mobile, filtres. | Slide |
|
||||
| `<Drawer>` | Variante simplifiée de Sheet (left/right uniquement, callback `onClose`). | Slide |
|
||||
| `<ConfirmDialog>` | Confirm destructive (focus initial sur Annuler). | = Dialog sm |
|
||||
|
||||
Tous backed Radix → focus trap, scroll lock, restitution focus, Esc, click backdrop.
|
||||
|
||||
## Dialog
|
||||
|
||||
```tsx
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title="Modifier l'utilisateur"
|
||||
description="Mettez à jour les informations."
|
||||
size="md" // sm | md | lg | xl | full
|
||||
footer={
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
||||
<Button variant="ghost" onClick={() => setOpen(false)}>Annuler</Button>
|
||||
<Button variant="primary" onClick={save}>Sauvegarder</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input label="Nom" />
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Sheet
|
||||
|
||||
```tsx
|
||||
<Sheet
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
side="right" // left | right | top | bottom
|
||||
size="md" // sm 320 | md 480 | lg 640 | xl 820 | full
|
||||
title="Édition rapide"
|
||||
description="Sauvegarde immédiate à chaque changement."
|
||||
footer={...}
|
||||
>
|
||||
<Form>...</Form>
|
||||
</Sheet>
|
||||
```
|
||||
|
||||
## ConfirmDialog (destructive)
|
||||
|
||||
```tsx
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
destructive
|
||||
title="Supprimer 3 collaborateurs ?"
|
||||
description="Cette action est irréversible."
|
||||
confirmLabel="Supprimer définitivement"
|
||||
onConfirm={() => api.delete(ids)}
|
||||
/>
|
||||
```
|
||||
|
||||
Focus initial sur **Annuler** (pas Confirm) pour éviter les actions destructives accidentelles.
|
||||
|
||||
## A11y
|
||||
|
||||
- `role="dialog"` + `aria-modal="true"` natif.
|
||||
- Title lié via `aria-labelledby`, description via `aria-describedby`.
|
||||
- Focus trap : Tab cycle uniquement dans le dialog.
|
||||
- Restitution focus à l'élément d'origine à la fermeture.
|
||||
- Esc ferme.
|
||||
- Click backdrop ferme (sauf si `modal={false}` posé).
|
||||
|
||||
## Do / Don't
|
||||
|
||||
✅ **Do**
|
||||
- Un titre explicite (RGAA 9.2 — relations).
|
||||
- Description si l'action a des conséquences (suppression, paiement).
|
||||
- Bouton primary à droite, ghost/cancel à gauche.
|
||||
|
||||
❌ **Don't**
|
||||
- Ne pas empiler les Dialogs (UX cauchemardesque). Si vraiment besoin, utiliser un Sheet + Wizard pattern.
|
||||
- Ne pas utiliser pour un message non-bloquant — préférer Toast.
|
||||
- Ne pas ouvrir automatiquement au load sans interaction utilisateur (RGAA 13.1 — pas d'apparition non sollicitée).
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Input / Textarea / Select
|
||||
description: Champs de formulaire DSMMG — Field-wrapped, accessibles, états couverts.
|
||||
---
|
||||
|
||||
## Anatomie
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Label (semi) * │ ← --required
|
||||
├──────────────────────────────────────┤
|
||||
│ [icon] Valeur saisie │ ← Input
|
||||
├──────────────────────────────────────┤
|
||||
│ Hint │ ← --tertiary
|
||||
│ Erreur │ ← --danger (si invalid)
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { Input, Textarea, Select } from "@managemate/react";
|
||||
|
||||
<Input
|
||||
label="E-mail"
|
||||
type="email"
|
||||
required
|
||||
hint="Nous ne partageons jamais votre e-mail."
|
||||
error={emailError} // string | undefined
|
||||
prefix={<Icon name="mail-line" />}
|
||||
size="md" // sm | md | lg
|
||||
/>
|
||||
|
||||
<Textarea label="Notes" rows={4} />
|
||||
|
||||
<Select
|
||||
label="Pays"
|
||||
options={[
|
||||
{ value: "fr", label: "France" },
|
||||
{ value: "be", label: "Belgique" },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Tailles
|
||||
|
||||
`sm` (28px), `md` (40px, défaut), `lg` (48px). La densité globale
|
||||
(`[data-mmg-density]`) ajuste automatiquement le padding.
|
||||
|
||||
## États
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Default | Border `--mmg-color-border` |
|
||||
| Hover | Border `--mmg-color-border-strong` |
|
||||
| Focus | Border accent + halo `--mmg-shadow-focus` |
|
||||
| Disabled | Bg `--mmg-color-state-disabled-bg`, cursor not-allowed |
|
||||
| Error | Border `--mmg-color-danger`, message `aria-describedby` |
|
||||
| Success | Border `--mmg-color-success` |
|
||||
|
||||
## Do / Don't
|
||||
|
||||
✅ **Do**
|
||||
|
||||
- Toujours fournir un `<Label>` visible (jamais le placeholder seul — RGAA 11.1).
|
||||
- `aria-describedby` automatique entre input et hint/error.
|
||||
- Validation côté serveur en complément (le client n'est jamais source de vérité).
|
||||
|
||||
❌ **Don't**
|
||||
|
||||
- Ne pas désactiver un input sans expliquer pourquoi (préférer rendre actif + bloquer la submit avec message).
|
||||
- Ne pas mettre du contenu HTML dans `placeholder` — il est lu par les lecteurs d'écran comme du texte.
|
||||
- Ne pas utiliser `<Select>` pour > 8 options — préférer `<Combobox>`.
|
||||
|
||||
## A11y
|
||||
|
||||
- `<label htmlFor>` natif, généré automatiquement.
|
||||
- `aria-invalid` posé quand `error` est fourni.
|
||||
- Touch target ≥ 44 (l'input md fait 40, mais un padding cliquable + label cliquable fait 48 effectif).
|
||||
- Erreurs annoncées via `role="alert"` quand elles apparaissent.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: MetricCard
|
||||
description: KPI dashboard — valeur géante, delta coloré, tendance, sparkline optionnel.
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { MetricCard, Sparkline } from "@managemate/react";
|
||||
|
||||
<MetricCard
|
||||
label="MRR"
|
||||
value="84 320 €"
|
||||
delta="+12.4%"
|
||||
trend="up"
|
||||
invertTrend={false} // true = "moins c'est mieux" (tickets, churn, latence)
|
||||
period="vs mois dernier"
|
||||
icon="money-euro-circle-line"
|
||||
sparkline={<Sparkline data={[20, 28, 25, 32, 30, 38, 42, 48]} width={200} height={48} />}
|
||||
href="/dashboard/mrr" // ou onClick
|
||||
/>
|
||||
```
|
||||
|
||||
## Sémantique trend
|
||||
|
||||
| `trend` | `invertTrend` | Couleur delta |
|
||||
|---|---|---|
|
||||
| `"up"` | `false` (défaut) | success (vert) |
|
||||
| `"up"` | `true` | danger (rouge) |
|
||||
| `"down"` | `false` | danger (rouge) |
|
||||
| `"down"` | `true` | success (vert) |
|
||||
| `"flat"` | — | neutre |
|
||||
|
||||
Exemple : "tickets ouverts" en hausse = mauvais → `trend="up" invertTrend`.
|
||||
|
||||
## Variantes interactives
|
||||
|
||||
- Si `href` ou `onClick` → la carte devient cliquable (drill-down vers la page de détail).
|
||||
- Hover : lift `translateY(-2px)` + border `--mmg-color-border-strong`.
|
||||
- Focus visible accent.
|
||||
|
||||
## Layout
|
||||
|
||||
Idéal en grille 4-cols :
|
||||
|
||||
```tsx
|
||||
<div className="mmg-grid mmg-grid--gap-md">
|
||||
{kpis.map(k => (
|
||||
<div key={k.id} className="mmg-col-3">
|
||||
<MetricCard {...k} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
## A11y
|
||||
|
||||
- Si interactif (`href`/`onClick`) : `<a>` ou `<button>` natif, focusable.
|
||||
- Le delta utilise icône + texte → couleur jamais seule (RGAA 9).
|
||||
- `font-variant-numeric: tabular-nums` sur valeur et delta pour alignement vertical.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: PricingCard
|
||||
description: Carte de tarification pour landings.
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { PricingCard, Button } from "@managemate/react";
|
||||
|
||||
<PricingCard
|
||||
name="Pro"
|
||||
description="Pour les PME en croissance."
|
||||
price="19 €"
|
||||
pricePeriod="/utilisateur/mois"
|
||||
highlighted // bordure accent + gradient subtil + ombre teintée
|
||||
badge="Populaire"
|
||||
features={[
|
||||
{ label: "Collaborateurs illimités" },
|
||||
{ label: "Support prioritaire 24/7" },
|
||||
{ label: "API & webhooks" },
|
||||
{ label: "SSO / SAML", included: false },
|
||||
]}
|
||||
cta={<Button variant="primary" block>Choisir Pro</Button>}
|
||||
/>
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### 3 plans avec le tier "Pro" highlighté
|
||||
|
||||
```tsx
|
||||
<div className="mmg-grid mmg-grid--gap-md">
|
||||
<div className="mmg-col-4"><PricingCard name="Starter" {...starter} /></div>
|
||||
<div className="mmg-col-4"><PricingCard name="Pro" {...pro} highlighted badge="Populaire" /></div>
|
||||
<div className="mmg-col-4"><PricingCard name="Enterprise" {...enterprise} /></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Features incluses / non incluses
|
||||
|
||||
`included: false` affiche une croix grise + texte barré + couleur quaternary. Permet de comparer rapidement les tiers.
|
||||
|
||||
## A11y
|
||||
|
||||
- Badge a `role` implicite — pas besoin de `aria-label` supplémentaire.
|
||||
- Liste `<ul>` sémantique pour les features.
|
||||
- Le CTA est un `<Button>` natif, focusable.
|
||||
|
||||
## Do / Don't
|
||||
|
||||
✅ **Do**
|
||||
- Maximum 3-5 features par tier (sinon utiliser un tableau de comparaison séparé).
|
||||
- Mettre `highlighted` sur **un seul** tier.
|
||||
- Prix proéminent (`display-md`-like).
|
||||
|
||||
❌ **Don't**
|
||||
- Pas plus de 4 tiers (surcharge cognitive).
|
||||
- Pas de prix barré sans contexte clair (utiliser `<Text strike>` dans le `price` si vraiment besoin).
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Slider
|
||||
description: Sélecteur de valeur — single ou range, clavier complet, focus halo.
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { Slider } from "@managemate/react";
|
||||
|
||||
// Single (volume, opacité, zoom)
|
||||
const [v, setV] = useState([62]);
|
||||
<Slider label="Volume" value={v} onValueChange={setV} min={0} max={100} step={1} />
|
||||
|
||||
// Range (fourchette de prix)
|
||||
const [range, setRange] = useState([800, 2400]);
|
||||
<Slider label="Prix" value={range} onValueChange={setRange} min={0} max={5000} step={50} />
|
||||
```
|
||||
|
||||
## Clavier
|
||||
|
||||
| Touche | Action |
|
||||
|---|---|
|
||||
| `←` / `↓` | Décrémente d'un step |
|
||||
| `→` / `↑` | Incrémente d'un step |
|
||||
| `PgUp` / `PgDn` | ±10 × step |
|
||||
| `Home` / `End` | Min / Max |
|
||||
| `Tab` | Passe au thumb suivant (range) |
|
||||
|
||||
## A11y
|
||||
|
||||
- `role="slider"` + `aria-valuenow/min/max/text` natif Radix.
|
||||
- `aria-label` sur chaque thumb.
|
||||
- Focus halo accent visible.
|
||||
- Touch target ≥ 44 (thumb 18px + zone tactile invisible).
|
||||
|
||||
## Affichage de la valeur
|
||||
|
||||
Au-dessus du slider, en label-adjacent (toujours visible, pas tooltip qui disparaît) :
|
||||
|
||||
```tsx
|
||||
<label style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span>Volume</span>
|
||||
<span style={{ fontVariantNumeric: "tabular-nums" }}>{v[0]}%</span>
|
||||
</label>
|
||||
<Slider value={v} onValueChange={setV} label="Volume" />
|
||||
```
|
||||
|
||||
## Do / Don't
|
||||
|
||||
✅ **Do**
|
||||
- Toujours afficher la valeur courante quelque part (label, tooltip, hint).
|
||||
- `step` cohérent avec l'unité (price step=50, volume step=5, opacité step=0.05).
|
||||
|
||||
❌ **Don't**
|
||||
- Pas de slider pour > 20 valeurs distinctes — préférer un Select numéroté.
|
||||
- Pas de slider pour des valeurs critiques sans confirmation (sliding peut déraper).
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: ThemePicker
|
||||
description: Sélecteur de couleur d'accent utilisateur.
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { ThemePicker, useAccent } from "@managemate/react";
|
||||
|
||||
<ThemePicker legend="Couleur d'accent" hideReset={false} />
|
||||
|
||||
// ou impératif
|
||||
const { accent, setAccent, reset } = useAccent();
|
||||
setAccent("blue");
|
||||
```
|
||||
|
||||
| Prop | Type | Défaut |
|
||||
|---|---|---|
|
||||
| `legend` | `string` | `"Couleur d'accent"` |
|
||||
| `hideReset` | `boolean` | `false` |
|
||||
| `className` | `string` | — |
|
||||
|
||||
## Pattern radiogroup
|
||||
|
||||
- `role="radiogroup"` avec `aria-labelledby` sur la légende.
|
||||
- Chaque pastille : `role="radio"` + `aria-checked` + `aria-label` (nom du preset).
|
||||
- **Roving tabindex** : seul le preset actif est `tabIndex=0`, les autres `-1`.
|
||||
- Navigation clavier :
|
||||
- `←/↑` preset précédent (wrap)
|
||||
- `→/↓` preset suivant (wrap)
|
||||
- `Home` premier preset
|
||||
- `End` dernier preset
|
||||
- Sélection à `Espace` ou en navigant aux flèches (sélection automatique au focus, pattern Radix RadioGroup).
|
||||
|
||||
## Persistance
|
||||
|
||||
Le hook `useAccent` :
|
||||
- Lit `localStorage["mmg-accent"]` au mount.
|
||||
- Écrit à chaque changement.
|
||||
- Pose `[data-mmg-accent="<preset>"]` sur `<html>`.
|
||||
|
||||
Pour éviter le flash au premier render (FOUC), inliner ce script dans `<head>` SSR :
|
||||
|
||||
```html
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var v = localStorage.getItem("mmg-accent");
|
||||
if (v && v !== "synapse") document.documentElement.setAttribute("data-mmg-accent", v);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
## Presets disponibles
|
||||
|
||||
| Preset | Hex (light) | Usage suggéré |
|
||||
|---|---|---|
|
||||
| `synapse` (défaut) | `#D12B6A` | Corporate ManageMate |
|
||||
| `rose` | `#E11D48` | Variante rose plus vif |
|
||||
| `blue` | `#2563EB` | Banking / fintech |
|
||||
| `violet` | `#7C3AED` | Forge, créatif, AI |
|
||||
| `green` | `#0E9F6E` (700) | HRTime, environnemental |
|
||||
| `amber` | `#D97706` (600) | Orbit, attention positive |
|
||||
| `red` | `#DC2626` | Sites événementiels |
|
||||
| `cyan` | `#0891B2` (700) | Analytics |
|
||||
| `slate` | `#475569` (700) | Neutre haut contraste |
|
||||
|
||||
Chaque preset est validé WCAG AA contre fonds light/dark + accent-on (texte sur CTA primary).
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Toast
|
||||
description: Notification temporaire empilable façon Sonner.
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { ToastProvider, useToast } from "@managemate/react";
|
||||
|
||||
// 1. Provider à la racine
|
||||
<ToastProvider position="bottom-right" max={5} visibleCount={3} defaultDuration={5000}>
|
||||
<App />
|
||||
</ToastProvider>
|
||||
|
||||
// 2. Dans un composant
|
||||
function Save() {
|
||||
const { toast, dismiss, clear } = useToast();
|
||||
return (
|
||||
<Button
|
||||
onClick={() => toast({
|
||||
title: "Modifications sauvegardées",
|
||||
description: "3 collaborateurs mis à jour.",
|
||||
severity: "success",
|
||||
duration: 5000,
|
||||
action: { label: "Annuler", onClick: () => undo() },
|
||||
})}
|
||||
>
|
||||
Sauvegarder
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Comportement (Sonner-style)
|
||||
|
||||
- **Au repos** : front-most pleine taille. Les autres empilés derrière avec `translateY` négatif et `scale` décroissant. 3 visibles max.
|
||||
- **Hover/focus** : la pile se déploie en colonne avec gap. Tous lisibles.
|
||||
- **Mouseleave/blur** : la pile se recolle.
|
||||
- **Pause des timers** au hover/focus, reprise au mouseleave.
|
||||
|
||||
## Severity
|
||||
|
||||
| Severity | Usage | Role ARIA |
|
||||
|---|---|---|
|
||||
| `info` | Information neutre | `status` (poli) |
|
||||
| `success` | Action réussie | `status` |
|
||||
| `warning` | Attention requise | `status` |
|
||||
| `danger` | Erreur, action échouée | `alert` (urgent) |
|
||||
|
||||
## Position
|
||||
|
||||
`bottom-right` (défaut), `bottom-left`, `top-right`, `top-left`.
|
||||
|
||||
## Do / Don't
|
||||
|
||||
✅ **Do**
|
||||
- 1-2 phrases max par toast.
|
||||
- `duration: 0` pour les erreurs critiques (l'utilisateur ferme).
|
||||
- Action `Annuler` pour les opérations réversibles.
|
||||
|
||||
❌ **Don't**
|
||||
- Pas de toast pour signaler du contenu important hors écran — utiliser `<Alert>` inline.
|
||||
- Pas de toast `danger` empilé en spam — limiter via `max={5}`.
|
||||
|
||||
## A11y
|
||||
|
||||
- `aria-live="polite"` sur la region (annonce non-disruptive).
|
||||
- `aria-atomic="false"` — chaque toast s'annonce individuellement.
|
||||
- `role="alert"` pour danger (urgent), `role="status"` sinon.
|
||||
- Bouton fermer focusable, label "Fermer la notification".
|
||||
- Pause au focus → l'utilisateur peut lire à son rythme.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: ToggleGroup
|
||||
description: Groupe de boutons toggle — single (radio) ou multiple (checkbox).
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { ToggleGroup } from "@managemate/react";
|
||||
|
||||
// Single
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
ariaLabel="Mode d'affichage"
|
||||
value={view}
|
||||
onValueChange={setView}
|
||||
items={[
|
||||
{ value: "list", label: "Liste", icon: "menu-line" },
|
||||
{ value: "grid", label: "Grille", icon: "apps-2-line" },
|
||||
{ value: "kanban", label: "Kanban", icon: "stack-line" },
|
||||
]}
|
||||
/>
|
||||
|
||||
// Multiple
|
||||
<ToggleGroup
|
||||
type="multiple"
|
||||
ariaLabel="Filtres statut"
|
||||
value={filters}
|
||||
onValueChange={setFilters} // string[]
|
||||
items={[
|
||||
{ value: "actif", label: "Actifs" },
|
||||
{ value: "absent", label: "Absents" },
|
||||
{ value: "conge", label: "En congé" },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
- `variant="outline"` (défaut) — fond muted, item actif sur surface + halo.
|
||||
- `variant="solid"` — item actif passe en accent plein.
|
||||
|
||||
## Sizes
|
||||
|
||||
`sm` (28px), `md` (defaut), `lg` (40px).
|
||||
|
||||
## A11y
|
||||
|
||||
Wrapper Radix UI ToggleGroup :
|
||||
- `role="group"` + `aria-label`
|
||||
- Roving tabindex (Tab cycle entre groupes, flèches dans le groupe)
|
||||
- `aria-pressed` sur chaque item
|
||||
|
||||
## Quand préférer Tabs / SegmentedControl / ToggleGroup
|
||||
|
||||
| | ToggleGroup | Tabs | SegmentedControl |
|
||||
|---|---|---|---|
|
||||
| Multi-select possible | ✓ | — | — |
|
||||
| Switch de **panels** | — | ✓ | — |
|
||||
| Switch de **vue/mode** | ✓ | ✓ | ✓ |
|
||||
| Style "pill segmented" | ✓ | — | ✓ |
|
||||
| Style "underline tabs" | — | ✓ | — |
|
||||
|
||||
Règle de pouce :
|
||||
- **Tabs** quand chaque option ouvre un panel de contenu différent.
|
||||
- **ToggleGroup multiple** quand on filtre une vue (filtres composables).
|
||||
- **SegmentedControl** = équivalent ToggleGroup single Apple HIG.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Tooltip
|
||||
description: Tooltip Radix UI — auto-flip, focus management, prefers-reduced-motion.
|
||||
---
|
||||
|
||||
## Quand utiliser
|
||||
|
||||
- Action courte (label d'un icon-only button : "Paramètres").
|
||||
- Information secondaire qui ne mérite pas de prendre de l'espace.
|
||||
|
||||
**Ne PAS utiliser** pour :
|
||||
- Information critique → utiliser `<Alert>` ou texte visible.
|
||||
- Contenu interactif (boutons, liens) → `Tooltip` est non-interactive. Utiliser `<Popover>` ou `<HoverCard>`.
|
||||
|
||||
## Setup
|
||||
|
||||
`<TooltipProvider>` à mettre **une seule fois** à la racine de l'app. Sans Provider, le Tooltip ne s'ouvre pas.
|
||||
|
||||
```tsx
|
||||
// app.tsx
|
||||
import { TooltipProvider } from "@managemate/react";
|
||||
|
||||
<TooltipProvider>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```tsx
|
||||
import { Tooltip, Button } from "@managemate/react";
|
||||
|
||||
<Tooltip content="Paramètres" placement="bottom" delay={200}>
|
||||
<Button variant="ghost" icon="settings-3-line" iconOnly aria-label="Paramètres" />
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
| Prop | Type | Défaut | |
|
||||
|---|---|---|---|
|
||||
| `content` | `ReactNode` | — | Contenu affiché |
|
||||
| `placement` | `"top"|"bottom"|"left"|"right"` | `"top"` | Auto-flip si pas de place |
|
||||
| `delay` | `number` | `200` | Délai d'apparition (ms) |
|
||||
| `sideOffset` | `number` | `8` | Décalage par rapport à la cible |
|
||||
|
||||
## A11y
|
||||
|
||||
- Géré par Radix — `role="tooltip"`, focus management, escape, hover-bridge.
|
||||
- Apparaît au **hover** ET au **focus clavier**.
|
||||
- Esc ferme et restitue le focus à l'élément déclencheur.
|
||||
- Respecte `prefers-reduced-motion`.
|
||||
|
||||
## Light vs Dark
|
||||
|
||||
| Mode | Background | Texte |
|
||||
|---|---|---|
|
||||
| Light | `neutral-900` (très foncé) | `neutral-0` (blanc) |
|
||||
| Dark | `bg-raised` + bord subtil | `text-primary` |
|
||||
|
||||
Le Tooltip ne se fond pas dans le bg-page sombre car il "pop" via élévation + bordure.
|
||||
|
||||
## Usage avancé
|
||||
|
||||
Pour des tooltips composés (avec image, multi-paragraphe), préférer `<HoverCard>`.
|
||||
|
||||
Pour piloter l'ouverture manuellement :
|
||||
|
||||
```tsx
|
||||
import { TooltipPrimitive } from "@managemate/react";
|
||||
|
||||
<TooltipPrimitive.Root open={isOpen} onOpenChange={setOpen}>
|
||||
<TooltipPrimitive.Trigger asChild>...</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content className="mmg-tooltip">...</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
</TooltipPrimitive.Root>
|
||||
```
|
||||
Reference in New Issue
Block a user