Metadata-Version: 2.4
Name: cicaw-sdui
Version: 0.1.1
Summary: Un système Server-Driven UI
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: pydantic>=2.0.0

# cicaw-sdui

**cicaw-sdui** est un framework Python de Server-Driven UI (SDUI) qui permet de décrire et sérialiser des interfaces utilisateur côté serveur, sous forme de JSON consommé par un frontend React (ou toute autre cible compatible).

---

## Table des matières

1. [Qu'est-ce que le Server-Driven UI ?](#quest-ce-que-le-server-driven-ui-)
2. [Pourquoi cicaw-sdui ?](#pourquoi-cicaw-sdui-)
3. [Installation](#installation)
4. [Architecture du package](#architecture-du-package)
5. [Concepts fondamentaux](#concepts-fondamentaux)
   - [UIComponent et rendu JSON](#uicomponent-et-rendu-json)
   - [Style](#style)
   - [Spacing](#spacing)
   - [Tokens de design system](#tokens-de-design-system)
6. [Composants — Atoms](#composants--atoms)
   - [Text](#text)
   - [Button](#button)
   - [Image / ImageView](#image--imageview)
   - [Icon](#icon)
   - [Badge](#badge)
   - [Avatar](#avatar)
   - [InputText](#inputtext)
   - [Switch / Checkbox](#switch--checkbox)
   - [Loader](#loader)
   - [MarkdownText](#markdowntext)
   - [GradientText / TextHighlight / WaveSeparator](#gradienttext--texthighlight--waveseparator)
   - [CartController / LoadingTrigger](#cartcontroller--loadingtrigger)
7. [Composants — Layouts](#composants--layouts)
   - [Screen](#screen)
   - [Column / Row](#column--row)
   - [Stack](#stack)
   - [Grid (via Style)](#grid-via-style)
   - [Carousel / GlideReel](#carousel--glidereel)
   - [Overlay](#overlay)
   - [Link](#link)
   - [Form](#form)
   - [Animated](#animated)
   - [GradientBackground / BackgroundImageContainer](#gradientbackground--backgroundimagecontainer)
   - [Transparent](#transparent)
8. [Actions](#actions)
   - [Actions client (immédiates)](#actions-client-immédiates)
   - [Actions serveur](#actions-serveur)
   - [Actions formulaire](#actions-formulaire)
   - [Chaînage d'actions](#chaînage-dactions)
9. [Tokens de référence](#tokens-de-référence)
10. [Exemples complets](#exemples-complets)
    - [Page d'accueil simple](#page-daccueil-simple)
    - [Formulaire de connexion](#formulaire-de-connexion)
    - [Card produit avec panier](#card-produit-avec-panier)
11. [Référence du rendu JSON](#référence-du-rendu-json)
12. [Contribuer](#contribuer)

---

## Qu'est-ce que le Server-Driven UI ?

Le Server-Driven UI (SDUI) est un paradigme dans lequel le **serveur décide de la structure et du contenu de l'interface**, et le **client se contente de la rendre**. Plutôt que d'avoir de la logique de navigation et de composition d'écrans dispersée dans le frontend, le backend retourne du JSON décrivant l'arbre de composants à afficher.

```
┌─────────────────────────────────────────────────────────────────┐
│  Client React                                                   │
│                                                                 │
│   GET /api/home ──────────────────► Backend Python (Django…)   │
│                                             │                   │
│   ◄──── JSON SDUI ──────────────────────────┘                  │
│                                                                 │
│   SDUIRenderer({                                                │
│     type: "screen",                                             │
│     data: { children: [...] }                                   │
│   })                                                            │
└─────────────────────────────────────────────────────────────────┘
```

**Avantages :**
- Déploiements sans mise à jour de l'app mobile (changement de textes, layouts, features flags…)
- Logique métier centralisée côté serveur
- A/B testing et personnalisation triviales
- UI identique sur toutes les plateformes (web, iOS, Android)

---

## Pourquoi cicaw-sdui ?

cicaw-sdui fournit une couche Python typée (Pydantic v2) pour :

- **Décrire** des interfaces via des classes Python lisibles et auto-documentées
- **Valider** la structure au moment de la construction (typage strict, énumérations)
- **Sérialiser** en JSON compact (`exclude_none=True`, `exclude_defaults=True`) prêt à être consommé par le renderer frontend
- **Composer** des vues complexes grâce à l'API fluente et aux conteneurs récursifs

---

## Installation

**Depuis PyPI :**
```bash
pip install cicaw-sdui
```

**Depuis les sources :**
```bash
git clone https://github.com/your-org/cicaw-sdui.git
cd cicaw-sdui
pip install -e .
```

**Prérequis :** Python ≥ 3.8, Pydantic ≥ 2.0.0

---

## Architecture du package

```
cicaw_sdui/
├── actions.py        # Actions client & serveur (navigation, formulaires, tracking…)
├── atoms.py          # Composants atomiques (Text, Button, Image, Icon, Badge…)
├── base.py           # Classes de base : UIComponent, Style, Spacing
├── enums.py          # Tous les tokens du design system (couleurs, tailles, espacements…)
└── layouts.py        # Conteneurs (Screen, Column, Row, Carousel, Form…)
```

Chaque fichier est indépendant et peut être importé séparément.

---

## Concepts fondamentaux

### UIComponent et rendu JSON

Tous les composants héritent de `UIComponent`. La méthode `.render()` produit le JSON final à retourner au client.

```python
from cicaw_sdui.atoms import Text
from cicaw_sdui.enums import TextSize, ColorRole

text = Text(
    text="Bienvenue",
    text_size=TextSize.XXXXL,
    text_color=ColorRole.TEXT_PRIMARY,
)

print(text.render())
# {
#   "type": "text",
#   "data": {
#     "text": "Bienvenue",
#     "text_size": "4xl",
#     "text_color": "text-primary"
#   }
# }
```

**Comportement de `.render()` :**
- Les champs `None` sont exclus (`exclude_none=True`)
- Les valeurs égales aux défauts sont exclues (`exclude_defaults=True`) — réduit la taille du JSON d'un facteur ~10
- Le résultat est toujours `{"type": "<component_type>", "data": {...}}`

---

### Style

Chaque `UIComponent` possède un objet `style: Style` qui regroupe toutes les propriétés CSS de bas niveau : dimensions, couleurs, flexbox, grilles, bordures, effets, transitions, etc.

```python
from cicaw_sdui.base import Style
from cicaw_sdui.enums import (
    ColorRole, RadiusToken, ElevationToken,
    SizingToken, SpacingToken
)

style = Style(
    width=SizingToken.FULL,
    background_color=ColorRole.SURFACE_RAISED,
    border_radius=RadiusToken.LG,
    elevation=ElevationToken.LEVEL_2,
    padding=SpacingToken.MD,
)
```

Les composants de type `Row` et `Column` exposent leurs propriétés de style directement (sans passer par `.style`) pour plus de lisibilité :

```python
from cicaw_sdui.layouts import Column
from cicaw_sdui.enums import GapToken, SpacingToken, ColorRole

column = Column(
    gap=GapToken.MD,
    padding_x=SpacingToken.LG,
    bg=ColorRole.SURFACE_MAIN,
)
```

---

### Spacing

Le modèle `Spacing` offre un contrôle fin des marges et paddings (par côté, par axe) indépendamment du `Style` :

```python
from cicaw_sdui.base import Spacing
from cicaw_sdui.enums import SpacingToken

spacing = Spacing(
    pt=SpacingToken.LG,
    pb=SpacingToken.MD,
    px=SpacingToken.GUTTER,
    mb=SpacingToken.XL,
)
```

---

### Tokens de design system

Tous les tokens sont des `Enum` string définis dans `enums.py`. Ils mappent directement sur les classes Tailwind / variables CSS du design system :

| Catégorie | Enum | Exemple |
|---|---|---|
| Espacement | `SpacingToken` | `SpacingToken.MD` → `p-md` (16px) |
| Dimensionnement | `SizingToken` | `SizingToken.FULL` → `w-full` |
| Couleurs | `ColorRole` | `ColorRole.PRIMARY` → `bg-primary` |
| Bordures | `RadiusToken` | `RadiusToken.LG` → `rounded-lg` |
| Élévation | `ElevationToken` | `ElevationToken.LEVEL_2` → `shadow-2` |
| Typographie | `TextSize`, `TextWeight` | `TextSize.XL` → `text-xl` |
| Animation | `AnimationToken` | `AnimationToken.FADE` |
| Flex | `MainAxisAlignment`, `CrossAxisAlignment` | `MainAxisAlignment.CENTER` |

---

## Composants — Atoms

### Text

Composant texte riche avec typographie complète.

```python
from cicaw_sdui.atoms import Text
from cicaw_sdui.enums import TextSize, TextWeight, ColorRole, TextAlign

# Titre principal
h1 = Text(
    text="Découvrez nos produits",
    text_size=TextSize.XXXXL,
    weight=TextWeight.BOLD,
    text_color=ColorRole.TEXT_HEADING,
    align=TextAlign.CENTER,
)

# Texte secondaire tronqué
caption = Text(
    text="Description longue qui sera coupée après 2 lignes...",
    text_size=TextSize.SM,
    text_color=ColorRole.TEXT_SECONDARY,
    max_lines=2,
)

# Texte animé (typewriter)
animated = Text(
    text="Chargement en cours…",
    animate=True,
    typewriter_speed=40,
)
```

**Propriétés clés :**

| Prop | Type | Description |
|---|---|---|
| `text` | `str` | Contenu textuel principal |
| `html` | `str` | HTML brut (dangerouslySetInnerHTML) |
| `text_size` | `TextSize` | `xs` / `sm` / `md` / `lg` / `xl` / `2xl` / `3xl` / `4xl` |
| `weight` | `TextWeight` | `light` / `regular` / `medium` / `semibold` / `bold` / `black` |
| `text_color` | `ColorRole` | Token de couleur du design system |
| `align` | `TextAlign` | `start` / `center` / `end` / `justify` |
| `max_lines` | `int` | Troncature multi-lignes via `-webkit-line-clamp` |
| `truncate` | `bool` | Troncature sur une seule ligne |
| `tag` | `TextTag` | Force la balise HTML (`h1`…`h6`, `p`, `span`…) |
| `typewriter_speed` | `int` | Délai entre caractères en ms |

---

### Button

```python
from cicaw_sdui.atoms import Button
from cicaw_sdui.enums import ButtonVariant, ButtonSize, ButtonState, IconName
from cicaw_sdui.actions import NavigateAction

btn = Button(
    label="Voir le catalogue",
    variant=ButtonVariant.FILLED,
    size=ButtonSize.LG,
    icon_right=IconName.ARROW_RIGHT,
    on_press=NavigateAction(route="/catalogue"),
)

# Bouton en état de chargement
loading_btn = Button(
    label="Connexion",
    variant=ButtonVariant.FILLED,
    state=ButtonState.LOADING,
    loading_label="Connexion en cours…",
)
```

**Variantes :**

| Valeur | Rendu |
|---|---|
| `FILLED` | Fond coloré, texte blanc — action principale |
| `TONAL` | Fond clair, texte primaire — action secondaire |
| `OUTLINED` | Bordure seule |
| `GHOST` | Texte seul, hover avec fond léger |
| `LINK` | Texte souligné |
| `FAB` | Bouton circulaire flottant |

---

### Image / ImageView

`Image` est un composant simple. `ImageView` est la version complète avec lazy-loading, aspect ratio, décoration et animation d'entrée.

```python
from cicaw_sdui.atoms import ImageView
from cicaw_sdui.enums import (
    AspectRatioToken, RadiusToken, ObjectFit, AnimationToken
)

# Image produit standard
product_img = ImageView(
    src="https://cdn.example.com/product-123.jpg",
    alt="Sneakers blanc taille 42",
    aspect_ratio=AspectRatioToken.SQUARE,
    object_fit=ObjectFit.COVER,
    radius=RadiusToken.MD,
    animate_in=AnimationToken.FADE,
)

# Hero section (above the fold, pas de lazy-load)
hero_img = ImageView(
    src="https://cdn.example.com/hero.jpg",
    alt="",
    aspect_ratio=AspectRatioToken.VIDEO,
    priority=True,   # eager load, LCP optimisé
)
```

---

### Icon

```python
from cicaw_sdui.atoms import Icon
from cicaw_sdui.enums import IconName, SizingToken, ColorRole

icon = Icon(
    name=IconName.HEART,
    size=SizingToken.S_6,      # 24px
    color=ColorRole.ERROR,
    stroke_width=1.5,
)
```

**Icônes disponibles** (sélection) :
`SHOPPING_CART`, `SEARCH`, `USER`, `HEART`, `STAR`, `ARROW_RIGHT`, `CHEVRON_RIGHT`, `CHECK`, `X`, `TRASH_2`, `EYE`, `UPLOAD`, `MAIL`, `PHONE`, `MAP_PIN`, `CLOCK`, `TRUCK`, `CREDIT_CARD`…

---

### Badge

```python
from cicaw_sdui.atoms import Badge
from cicaw_sdui.enums import ColorRole, IconName

# Badge texte
Badge(label="Nouveau", color=ColorRole.PRIMARY, tone="filled")

# Badge compteur
Badge(count=5, color=ColorRole.ERROR, tone="filled", max_count=99)

# Badge point (notification dot)
Badge(dot=True, color=ColorRole.SUCCESS)

# Badge avec icône
Badge(label="Pro", icon=IconName.STAR, tone="outlined", color=ColorRole.WARNING)
```

---

### Avatar

```python
from cicaw_sdui.atoms import Avatar

# Avec image
Avatar(src="https://cdn.example.com/user.jpg", size="lg")

# Avec initiales (fallback)
Avatar(initials="JD", size="md")
```

---

### InputText

```python
from cicaw_sdui.atoms import InputText
from cicaw_sdui.enums import InputType

InputText(
    name="email",
    label="Adresse e-mail",
    type=InputType.TEXT,
    placeholder="vous@exemple.com",
    required=True,
    help_text="Nous ne partageons jamais votre adresse.",
)

InputText(
    name="password",
    label="Mot de passe",
    type=InputType.PASSWORD,
    min_length=8,
    max_length=128,
)

# Select (liste déroulante)
InputText(
    name="country",
    label="Pays",
    widget="select",
    choices=[
        {"value": "SN", "label": "Sénégal"},
        {"value": "CI", "label": "Côte d'Ivoire"},
        {"value": "ML", "label": "Mali"},
    ],
)
```

---

### Switch / Checkbox

```python
from cicaw_sdui.atoms import Switch, Checkbox

Switch(name="notifications", label="Activer les notifications", value=True)
Checkbox(name="cgu", label="J'accepte les conditions générales", value=False)
```

---

### Loader

```python
from cicaw_sdui.atoms import Loader

Loader(size="md")   # sm | md | lg
```

---

### MarkdownText

Rendu de contenu Markdown riche (GFM : tableaux, listes, blocs de code…) de façon sécurisée.

```python
from cicaw_sdui.atoms import MarkdownText

MarkdownText(
    text="## Guide d'utilisation\n\nVoici les **étapes** pour commencer :\n\n1. Créez un compte\n2. Ajoutez vos produits\n3. Partagez votre boutique",
    prose_size="base",       # sm | base | lg
    open_links_in_new_tab=True,
)
```

---

### GradientText / TextHighlight / WaveSeparator

```python
from cicaw_sdui.atoms import GradientText, TextHighlight, WaveSeparator
from cicaw_sdui.enums import (
    GradientTextVariant, GradientDirection,
    TextHighlightVariant, WaveSeparatorVariant, ColorRole
)

# Texte en dégradé
GradientText(
    text="Bienvenue sur Cicaw",
    variant=GradientTextVariant.LINEAR,
    from_color=ColorRole.PRIMARY,
    to_color=ColorRole.ACCENT,
    direction=GradientDirection.TO_RIGHT,
    tag="h1",
)

# Mise en valeur de mots-clés
TextHighlight(
    text="Livraison gratuite et retours faciles.",
    highlighted_words="gratuite,faciles",
    variant=TextHighlightVariant.MARKER,
    color=ColorRole.WARNING,
)

# Séparateur décoratif entre sections
WaveSeparator(
    variant=WaveSeparatorVariant.WAVE,
    color=ColorRole.SURFACE_MAIN,
    height=80,
    flip_y=True,
)
```

---

### CartController / LoadingTrigger

```python
from cicaw_sdui.atoms import CartController, LoadingTrigger

# Contrôleur d'ajout au panier (bouton +/- intégré)
CartController(
    product_id=42,
    initial_quantity=0,
    add_label="Ajouter",
    add_url="/api/cart/add",
    remove_url="/api/cart/remove",
)

# Déclencheur de chargement (infinite scroll)
LoadingTrigger(
    url="/api/products?page=2",
    label="Charger plus",
)
```

---

## Composants — Layouts

### Screen

Conteneur racine de chaque page SDUI. Retourné par vos vues Django/FastAPI/Flask.

```python
from cicaw_sdui.layouts import Screen

def home_view(request):
    screen = Screen(
        page_title="Accueil — Cicaw",
        page_description="Découvrez nos produits",
        track_page_view="home_viewed",
        stale_time=60_000,           # Cache React Query : 1 minute
        cache_strategy="normal",
        children=[...]
    )
    return screen.render()
```

**Propriétés clés :**

| Prop | Type | Description |
|---|---|---|
| `page_title` | `str` | Titre de l'onglet |
| `page_description` | `str` | Meta description |
| `stale_time` | `int` | Cache React Query en ms |
| `cache_strategy` | `str` | `normal` / `aggressive` / `no-cache` |
| `track_page_view` | `str` | Nom d'event analytics au montage |
| `max_width` | `int` | max-width du conteneur en px |
| `progress_bar` | `bool` | Affiche NProgress pendant le chargement |

---

### Column / Row

Les deux briques de layout les plus utilisées. Exposent toutes les propriétés flex + espacement + visuel directement.

```python
from cicaw_sdui.layouts import Column, Row
from cicaw_sdui.enums import (
    GapToken, SpacingToken, SizingToken,
    CrossAxisAlignment, MainAxisAlignment, ColorRole, RadiusToken
)

# Colonne verticale centrée
Column(
    gap=GapToken.MD,
    padding_x=SpacingToken.GUTTER,
    padding_y=SpacingToken.LG,
    cross_axis_align=CrossAxisAlignment.CENTER,
    width=SizingToken.FULL,
    children=[...],
)

# Ligne horizontale avec espace entre les éléments
Row(
    main_axis_align=MainAxisAlignment.SPACE_BETWEEN,
    cross_axis_align=CrossAxisAlignment.CENTER,
    padding_x=SpacingToken.MD,
    bg=ColorRole.SURFACE_HEADER,
    children=[logo, nav_actions],
)
```

**API fluente :**

```python
col = Column(gap=GapToken.SM).justify_center().items_center()
col.with_children(text1, text2, text3)
```

---

### Stack

Superpose les enfants sur l'axe Z (position relative/absolute).

```python
from cicaw_sdui.layouts import Stack, Overlay
from cicaw_sdui.enums import OverlayPlacement

Stack(
    children=[
        image_component,
        Overlay(
            placement=OverlayPlacement.BOTTOM_LEFT,
            children=[badge_promo],
        ),
    ]
)
```

---

### Grid (via Style)

La grille CSS passe par `Style.grid_cols` sur un `Column` ou `Row`.

```python
from cicaw_sdui.layouts import Row
from cicaw_sdui.base import Style
from cicaw_sdui.enums import GridCols, SpacingToken

Row(
    style=Style(
        grid_cols=GridCols.THREE,
        gap=SpacingToken.MD,
    ),
    children=[card1, card2, card3],
)
```

---

### Carousel / GlideReel

```python
from cicaw_sdui.layouts import Carousel, GlideReel
from cicaw_sdui.enums import CarouselEffect

# Carousel produits responsive
carousel = Carousel(
    children=[slide1, slide2, slide3],
)
carousel.layout(per_view=1.2, space=16)
carousel.set_responsive(sm=1.2, md=2.2, lg=3.5)
carousel.navigation(dots=True, arrows=False)
carousel.set_autoplay(delay=4000)

# GlideReel avancé
reel = GlideReel(
    slidesPerView="auto",
    spaceBetween=16,
    loop=True,
    navigation=True,
    pagination=GlidePagination.BULLETS,
    autoplay=GlideAutoplayConfig(delay=3000, pauseOnHover=True),
    children=[...],
)
```

---

### Overlay

Positionne son contenu en absolu dans un `Stack` parent.

```python
from cicaw_sdui.layouts import Overlay
from cicaw_sdui.enums import (
    OverlayPlacement, OverlayBackdrop, AnimationToken
)

Overlay(
    placement=OverlayPlacement.BOTTOM_CENTER,
    backdrop=OverlayBackdrop.GRADIENT_BOTTOM,
    animation=AnimationToken.SLIDE_UP,
    children=[title_text, cta_button],
)
```

---

### Link

```python
from cicaw_sdui.layouts import Link

Link(
    url="/blog/article-1",
    target="_self",
    children=[Text(text="Lire l'article")],
)

# Lien externe dans un nouvel onglet
Link(url="https://example.com").open_in_new_tab().with_children(icon)
```

---

### Form

Conteneur de formulaire. Combiné à `FormSubmitAction` et `FormScope`.

```python
from cicaw_sdui.layouts import Form, Column
from cicaw_sdui.atoms import InputText, Button
from cicaw_sdui.enums import InputType, ButtonVariant
from cicaw_sdui.actions import FormSubmitAction, FieldRule

form = Form(
    form_id="register_form",
    children=[
        Column(
            gap=GapToken.MD,
            children=[
                InputText(name="email", label="E-mail", type=InputType.TEXT),
                InputText(name="password", label="Mot de passe", type=InputType.PASSWORD),
                Button(
                    label="Créer mon compte",
                    variant=ButtonVariant.FILLED,
                    on_press=FormSubmitAction(
                        form_id="register_form",
                        endpoint="/api/auth/register",
                        rules=[
                            FieldRule(field="email", required=True, pattern=r".+@.+\..+"),
                            FieldRule(field="password", required=True, min_length=8),
                        ],
                    ),
                ),
            ],
        )
    ],
)
```

---

### Animated

Enveloppe n'importe quel composant ou sous-arbre avec une animation CSS/JS.

```python
from cicaw_sdui.layouts import Animated
from cicaw_sdui.enums import AnimationVariant, AnimationTrigger

# Animation déclenchée à l'entrée dans le viewport
Animated(
    variant=AnimationVariant.FADE_UP,
    trigger=AnimationTrigger.VISIBLE,
    delay_ms=100,
    duration_ms=600,
    children=[my_card],
)

# Animer une liste avec stagger (enfants animés un par un)
Animated(
    variant=AnimationVariant.STAGGER_UP,
    stagger_ms=80,
    trigger=AnimationTrigger.VISIBLE,
    children=[item1, item2, item3, item4],
)
```

**Variantes d'entrée** (sélection) :
`FADE_IN`, `FADE_UP`, `FADE_DOWN`, `ZOOM_IN`, `SLIDE_UP`, `BOUNCE_IN`, `BLUR_IN`, `REVEAL_CLIP`

**Variantes en boucle** :
`PULSE`, `FLOAT`, `BREATHE`, `SPIN`, `GLOW`, `SHIMMER_LOOP`

**Variantes stagger** :
`STAGGER_UP`, `STAGGER_WAVE`, `STAGGER_CASCADE`, `STAGGER_FADE`

---

### GradientBackground / BackgroundImageContainer

```python
from cicaw_sdui.layouts import GradientBackground, BackgroundImageContainer
from cicaw_sdui.enums import (
    GradientVariant, GradientDirection, ColorRole,
    BgVariant, BgPosition, BgSize
)

# Section avec fond dégradé
GradientBackground(
    variant=GradientVariant.LINEAR,
    from_color=ColorRole.PRIMARY,
    to_color=ColorRole.SECONDARY,
    direction=GradientDirection.TO_BOTTOM,
    children=[hero_content],
)

# Section hero avec image de fond
BackgroundImageContainer(
    src="https://cdn.example.com/hero.jpg",
    alt="",
    position=BgPosition.CENTER,
    size=BgSize.COVER,
    variant=BgVariant.OVERLAY_GRADIENT_BOTTOM,
    lazy=False,
    children=[title, subtitle, cta],
)
```

---

### Transparent

Fragment sans wrapper DOM — retourne ses enfants directement.

```python
from cicaw_sdui.layouts import Transparent

# Utile pour les fragments de données (DataSource retournant plusieurs frères)
Transparent(children=[section_a, section_b, section_c])
```

---

## Actions

Les actions décrivent **ce qui se passe** quand l'utilisateur interagit (clic, soumission, etc.) ou quand le serveur répond. Elles sont passées aux props `on_press`, `on_click`, `on_submit` ou dans les réponses API.

### Actions client (immédiates)

Exécutées localement sans appel réseau.

```python
from cicaw_sdui.actions import (
    NavigateAction, ToastAction, OpenUrlAction
)

# Navigation interne
NavigateAction(route="/profil")
NavigateAction(route="/home", replace=True)   # Remplace l'historique

# Toast / notification
ToastAction(message="Produit ajouté au panier !", level="success", duration=3000)
ToastAction(message="Erreur réseau", title="Oops", level="error", dismissible=True)

# Lien externe
OpenUrlAction(url="https://wa.me/221XXXXXXXXX")
```

---

### Actions serveur

Envoient une requête au backend et exécutent la réponse (une nouvelle `ActionUnion`).

```python
from cicaw_sdui.actions import (
    Post, ServerEventAction, RefreshScreenAction,
    RemoveComponent, UpdateComponentAction,
    InsertComponentAction, FetchInsertAction, FetchAction
)

# POST vers un endpoint
Post(pathname="/api/products/42/like", show_loader=True)

# Rafraîchir la page entière
RefreshScreenAction(show_loader=True)

# Supprimer un composant de l'arbre (ex: supprimer un item de liste)
RemoveComponent(target_id="product-card-42")

# Modifier partiellement un composant (optimistic UI)
UpdateComponentAction(
    target_id="like-btn-42",
    data_patch={"icon": "heart", "color": "error"},
)

# Insérer du SDUI dynamiquement (infinite scroll, pagination)
FetchInsertAction(
    url="/api/products?page=2",
    target_id="product-list",
    mode="append",
    remove_trigger=True,
)

# Charger et exécuter une action depuis le serveur
FetchAction(
    url="/api/next-step",
    method="post",
    payload={"context": "onboarding"},
    show_loader=True,
)
```

---

### Actions formulaire

```python
from cicaw_sdui.actions import (
    FormSubmitAction, FormResetAction, FormSetErrorsAction, FieldRule
)

# Soumettre un formulaire avec validation côté client
FormSubmitAction(
    form_id="login_form",
    endpoint="/api/auth/login",
    method="post",
    rules=[
        FieldRule(field="email", required=True, pattern=r".+@.+\..+", message="Email invalide"),
        FieldRule(field="password", required=True, min_length=8),
    ],
    show_loader=True,
    reset_on_success=False,
)

# Réinitialiser un formulaire
FormResetAction(form_id="login_form")

# Injecter des erreurs serveur dans le formulaire (422 / logique métier)
FormSetErrorsAction(
    form_id="register_form",
    errors={"email": "Cette adresse est déjà utilisée."},
)
```

---

### Chaînage d'actions

Toute action peut enchaîner une autre via `on_success`, `on_error`, `next_action`, ou être groupée dans un `BatchAction`.

```python
from cicaw_sdui.actions import (
    Post, ToastAction, NavigateAction,
    TrackAction, BatchAction, RemoveComponent
)

# Après suppression : toast + suppression du composant
Post(
    pathname="/api/products/42/delete",
    on_success=BatchAction(
        mode="sequence",
        actions=[
            ToastAction(message="Produit supprimé", level="success"),
            RemoveComponent(target_id="product-card-42"),
        ],
    ),
    on_error=ToastAction(message="Erreur lors de la suppression", level="error"),
)

# Tracking + navigation
TrackAction(
    event_name="cta_clicked",
    properties={"source": "home_banner"},
    next_action=NavigateAction(route="/catalogue"),
)
```

---

## Tokens de référence

### SpacingToken (padding, margin, gap)

| Token | Valeur CSS |
|---|---|
| `XXXS` | 2px |
| `XXS` | 4px |
| `XS` | 8px |
| `SM` | 12px |
| `MD` | 16px |
| `LG` | 24px |
| `XL` | 32px |
| `XXL` | 48px |
| `XXXL` | 64px |
| `GUTTER` | 20px |

### TextSize

| Token | Taille |
|---|---|
| `XS` | 12px |
| `SM` | 14px |
| `MD` | 16px (défaut) |
| `LG` | 18px |
| `XL` | 20px |
| `XXL` | 24px |
| `XXXL` | 30px |
| `XXXXL` | 36px |

### ColorRole (sélection)

| Token | Usage |
|---|---|
| `SURFACE_MAIN` | Fond principal de la page |
| `SURFACE_RAISED` | Fond de carte légèrement élevée |
| `TEXT_PRIMARY` | Texte principal |
| `TEXT_SECONDARY` | Texte secondaire (moins visible) |
| `TEXT_DISABLED` | Texte désactivé |
| `PRIMARY` | Couleur de marque principale |
| `SUCCESS` | Validation, succès |
| `ERROR` | Erreurs, états destructifs |
| `WARNING` | Avertissements |
| `BORDER_DEFAULT` | Bordure standard |
| `ALWAYS_WHITE` | Blanc forcé (même en Dark Mode) |

---

## Exemples complets

### Page d'accueil simple

```python
from cicaw_sdui.layouts import Screen, Column, Row
from cicaw_sdui.atoms import Text, Button, ImageView
from cicaw_sdui.actions import NavigateAction
from cicaw_sdui.enums import (
    TextSize, TextWeight, ColorRole, ButtonVariant,
    GapToken, SpacingToken, SizingToken, AspectRatioToken
)

def home_screen():
    return Screen(
        page_title="Accueil",
        track_page_view="home_viewed",
        children=[
            Column(
                gap=GapToken.LG,
                padding_x=SpacingToken.GUTTER,
                padding_y=SpacingToken.XL,
                children=[
                    Text(
                        text="Bienvenue sur Cicaw",
                        text_size=TextSize.XXXXL,
                        weight=TextWeight.BOLD,
                    ),
                    Text(
                        text="Découvrez des milliers de produits.",
                        text_size=TextSize.LG,
                        text_color=ColorRole.TEXT_SECONDARY,
                    ),
                    Button(
                        label="Explorer le catalogue",
                        variant=ButtonVariant.FILLED,
                        on_press=NavigateAction(route="/catalogue"),
                    ),
                ],
            )
        ],
    ).render()
```

---

### Formulaire de connexion

```python
from cicaw_sdui.layouts import Screen, Column, Form
from cicaw_sdui.atoms import Text, InputText, Button
from cicaw_sdui.actions import (
    AuthLoginAction, FormSubmitAction, FieldRule, NavigateAction, ToastAction
)
from cicaw_sdui.enums import (
    InputType, ButtonVariant, TextSize, TextWeight,
    GapToken, SpacingToken, SizingToken
)

def login_screen():
    return Screen(
        page_title="Connexion",
        children=[
            Column(
                gap=GapToken.LG,
                padding=SpacingToken.XL,
                max_width=SizingToken.MD,
                margin_x=SpacingToken.AUTO,
                children=[
                    Text(
                        text="Connectez-vous",
                        text_size=TextSize.XXXL,
                        weight=TextWeight.BOLD,
                    ),
                    Form(
                        form_id="login_form",
                        children=[
                            Column(
                                gap=GapToken.MD,
                                children=[
                                    InputText(
                                        name="email",
                                        label="Adresse e-mail",
                                        type=InputType.TEXT,
                                        required=True,
                                    ),
                                    InputText(
                                        name="password",
                                        label="Mot de passe",
                                        type=InputType.PASSWORD,
                                        required=True,
                                    ),
                                    Button(
                                        label="Se connecter",
                                        variant=ButtonVariant.FILLED,
                                        on_press=FormSubmitAction(
                                            form_id="login_form",
                                            endpoint="/api/auth/login",
                                            rules=[
                                                FieldRule(field="email", required=True),
                                                FieldRule(field="password", required=True, min_length=6),
                                            ],
                                            on_success=NavigateAction(route="/dashboard"),
                                            on_error=ToastAction(
                                                message="Identifiants incorrects",
                                                level="error",
                                            ),
                                        ),
                                    ),
                                ],
                            )
                        ],
                    ),
                ],
            )
        ],
    ).render()
```

---

### Card produit avec panier

```python
from cicaw_sdui.layouts import Column, Row, Stack, Overlay
from cicaw_sdui.atoms import (
    ImageView, Text, Badge, CartController
)
from cicaw_sdui.enums import (
    AspectRatioToken, RadiusToken, ColorRole,
    TextSize, TextWeight, GapToken, SpacingToken,
    ElevationToken, OverlayPlacement
)

def product_card(product: dict) -> dict:
    return Stack(
        style=Style(
            border_radius=RadiusToken.LG,
            elevation=ElevationToken.LEVEL_1,
            overflow=Overflow.HIDDEN,
        ),
        children=[
            ImageView(
                src=product["image"],
                alt=product["name"],
                aspect_ratio=AspectRatioToken.SQUARE,
            ),
            # Badge promo en superposition
            Overlay(
                placement=OverlayPlacement.TOP_LEFT,
                children=[
                    Badge(
                        label=f"-{product['discount']}%",
                        color=ColorRole.ERROR,
                        tone="filled",
                    )
                ] if product.get("discount") else [],
            ),
            # Infos produit
            Column(
                gap=GapToken.XS,
                padding=SpacingToken.MD,
                children=[
                    Text(
                        text=product["name"],
                        text_size=TextSize.SM,
                        weight=TextWeight.SEMIBOLD,
                        max_lines=2,
                    ),
                    Row(
                        main_axis_align=MainAxisAlignment.SPACE_BETWEEN,
                        cross_axis_align=CrossAxisAlignment.CENTER,
                        children=[
                            Text(
                                text=f"{product['price']} FCFA",
                                text_size=TextSize.MD,
                                weight=TextWeight.BOLD,
                                text_color=ColorRole.PRIMARY,
                            ),
                            CartController(
                                product_id=product["id"],
                                add_url="/api/cart/add",
                                remove_url="/api/cart/remove",
                            ),
                        ],
                    ),
                ],
            ),
        ],
    ).render()
```

---

## Référence du rendu JSON

Chaque `UIComponent.render()` produit un objet de la forme :

```json
{
  "type": "<component_type>",
  "data": {
    "id": "optional-id",
    "prop1": "value1",
    "...": "...",
    "children": [
      {
        "type": "<child_type>",
        "data": { "...": "..." }
      }
    ]
  }
}
```

Les actions sont sérialisées avec leur `type` directement dans l'objet :

```json
{
  "type": "navigate",
  "route": "/catalogue",
  "replace": false
}
```

**Optimisation :** `.render()` utilise `exclude_none=True` et `exclude_defaults=True`. Un composant minimal comme `Text(text="Hello")` ne produira que les champs explicitement définis, réduisant drastiquement la taille du payload.

---

## Contribuer

1. Forkez le dépôt et créez une branche : `git checkout -b feat/mon-composant`
2. Ajoutez votre composant dans le fichier approprié (`atoms.py`, `layouts.py` ou `actions.py`)
3. Déclarez le type dans `ComponentType` (ou `ActionType`) dans `enums.py`
4. Ajoutez les tests correspondants
5. Ouvrez une Pull Request

**Conventions :**
- Les classes Python sont en `PascalCase`
- Les valeurs d'énumérations sont en `snake_case` (pour correspondre aux classes Tailwind/CSS)
- Toutes les propriétés doivent être typées et documentées via des docstrings ou des `Field(description=...)`
- Préférer `Optional[X] = None` aux valeurs par défaut implicites pour tirer parti de `exclude_defaults=True`

---

*cicaw-sdui est maintenu par l'équipe Cicaw.*
