feat(v1): bloquants release v1 — tests, stories, visual regression, gouvernance, publishing
Release / Release / open changeset PR (push) Has been cancelled
CI / Build, typecheck, test, a11y (push) Has been cancelled

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:
Dinawo
2026-05-04 22:29:35 +02:00
parent 62317f2ad7
commit 133feff75d
69 changed files with 3433 additions and 7 deletions
@@ -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).
+80
View File
@@ -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).
+72
View File
@@ -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>
```