{#- Macros partagées. À importer : {% from "_composants.html" import alerte, zone_etat %} Aucune de ces macros ne lit le contexte de la page : tout passe par les arguments. Elles posent les attributs `data-*` que lit `statique/ilot.js` (liste fermée de la spec § 1.3) ; les libellés que l'îlot affiche passent par `data-libelle-*`. Contenu : zone_etat, alerte, vie_privee, attributs_brouillon (attributs d'un formulaire modifiable), bouton_enregistrer, barre_enregistrement, actions, bloc_actions, actions_fiche, action_principale, toast, avancement, pastilles_motifs et onglets_page. -#} {#- Zone d'état d'un formulaire ou d'une page : cible des erreurs (`HX-Retarget: #etat-`, plan §2.10). `contenu` : message initial facultatif. -#} {% macro zone_etat(zone, contenu="") -%}
{{ contenu }}
{%- endmacro %} {#- Alerte d'erreur (`role="alert"`). -#} {% macro alerte(message) -%} {%- endmacro %} {#- Texte de vie privée : une fois par zone IA (spec § 1.6), jamais sous chaque bouton. sorte : "offres" (page Offres, sous les onglets), "fiche" (sous l'en-tête de la fiche quand une préparation est proposée), "ia" (bandeau de la lettre), "ia_cv" (bandeau du CV adapté), "ia_cv_libre" (bandeau du CV adapté en mode libre), "ia_cv_base" (bandeau de Mon CV), "creation" (parcours guidé de Mon CV), "documents" (recréer le CV depuis les documents), "gabarit" (écrire l'adaptateur du gabarit Typst personnel). "generation" : ancien nom de "offres", gardé jusqu'à CB1. -#} {% macro vie_privee(sorte="ia") -%} {% if sorte in ("offres", "fiche", "generation") -%} Préparer un dossier envoie l'offre, vos règles de rédaction et votre CV de base à Claude (Anthropic). {%- elif sorte == "ia_cv" -%} Le CV (ou l'élément choisi), l'offre, vos règles et votre profil sont envoyés à Claude (Anthropic). Cliquez dans une puce ou dans l'accroche pour ne réécrire qu'elle. {%- elif sorte == "ia_cv_libre" -%} Le CV (ou le passage choisi), l'offre, vos règles et votre profil sont envoyés à Claude (Anthropic). {%- elif sorte == "ia_cv_base" -%} Le CV (ou l'élément choisi), vos règles et votre profil sont envoyés à Claude (Anthropic). Cliquez dans une puce ou dans l'accroche pour ne réécrire qu'elle. {%- elif sorte == "creation" -%} Les documents, votre consigne, vos règles de rédaction et votre profil sont envoyés à Claude (Anthropic). Rien n'est inventé : ce qui manque dans vos documents reste à compléter. {%- elif sorte == "documents" -%} L'IA propose un nouveau CV ; vous le comparez à l'actuel, élément par élément, avant d'accepter. Les documents cochés, vos règles et votre profil sont envoyés à Claude (Anthropic). {%- elif sorte == "gabarit" -%} Les fichiers Typst du gabarit et de votre CV d'exemple, ainsi que votre consigne, sont envoyés à Claude (Anthropic). Les images et polices restent sur le poste ; ni profil ni règles de rédaction ne sont envoyés. {%- else -%} La lettre (ou le passage choisi), l'offre, vos règles et votre profil sont envoyés à Claude (Anthropic). {%- endif %} {%- endmacro %} {#- Attributs d'un `
` dont le brouillon est suivi par l'îlot : modifié ou non, état « modifié » (`data-libelle-modifie`), proposition en attente, garde de sortie. À poser dans la balise : ``. `modifie` : le serveur rend un brouillon qui diffère du fichier (`data-modifie="1"`) ; `objet` : de quoi parle la garde de sortie (« La lettre », « Le CV »...) ; `libelle_modifie` : texte d'état d'un formulaire modifié (propre à la page). -#} {% macro attributs_brouillon(modifie=False, objet="La page", libelle_modifie="● Modifications non enregistrées") -%} data-brouillon{% if modifie %} data-modifie="1"{% endif %} data-libelle-modifie="{{ libelle_modifie }}" data-libelle-proposition="Une proposition de l'IA attend votre décision." data-libelle-garde-titre="Modifications non enregistrées" data-libelle-garde-texte="{{ objet }} a changé depuis le dernier enregistrement. Que voulez-vous faire avant de quitter ?" data-libelle-rester="Rester" data-libelle-quitter="Quitter sans enregistrer" data-libelle-enregistrer-quitter="Enregistrer et quitter" {%- endmacro %} {#- Le bouton Enregistrer (envoi du formulaire). `oob` : version échangée hors bande, pour une réponse partielle qui doit le désactiver (proposition en attente). -#} {% macro bouton_enregistrer(zone, libelle="Enregistrer", desactive=False, oob=False) -%} {%- endmacro %} {#- Barre d'enregistrement (spec § 1.2), à poser en dernier dans le ``. Collante en bas de page. `zone` : suffixe des ids (`#etat-` reçoit aussi les erreurs) ; `etat` : texte initial (« ✓ Version 3, enregistrée il y a 2 h ») ; `modifie` : le formulaire diffère du fichier (Annuler visible, Enregistrer actif) ; `boutons` : enregistrer libellé du bouton (« Enregistrer ») annuler {url, cible, select} : GET qui recharge l'éditeur depuis le disque ; `cible` (défaut `#formulaire-`) et `select` (défaut `cible`) telecharger {url, libelle} : lien « Télécharger le PDF » proposition_en_attente vrai : Enregistrer désactivé L'îlot met à jour l'état, Annuler et Enregistrer quand l'utilisateur saisit. -#} {% macro barre_enregistrement(zone, etat, modifie, boutons) -%} {% set b = boutons or {} %} {% set annuler = b.get("annuler") %} {% set telecharger = b.get("telecharger") %}
{{ etat }}
{% if telecharger %} {{ telecharger.get("libelle") or "Télécharger le PDF" }} {% endif %} {% if annuler %} {% set cible = annuler.get("cible") or "#formulaire-" ~ zone %} {% endif %} {{ bouton_enregistrer(zone, b.get("enregistrer") or "Enregistrer", desactive=(not modifie) or b.get("proposition_en_attente")) }}
{%- endmacro %} {#- Une action (bouton, lien, entrée de menu) d'un `contexte.Action`. -#} {% macro _action(a, cible, principale=False, dans_menu=False) -%} {% set classes = "bouton-menu" if dans_menu else ("bouton principal" if principale else "bouton") %} {% if a.methode == "lien" -%} {{ a.libelle }} {%- elif a.desactive or a.occupe -%} {%- else -%} {%- endif %} {%- endmacro %} {#- Actions d'une offre (règle du § 1.5 : un menu « ⋯ » à partir de trois actions, compte de la principale compris ; « Effacer le dossier… » est toujours dans le menu). `principale`, `autres`, `reservee_au_menu`, `etiquette` : ceux d'un `contexte.Actions` ; `id` : id de l'offre ; `titre` : nom lu par les lecteurs d'écran (« Autres actions pour ») ; `cible` : élément que remplace une action `post` (défaut : la carte `#carte-`). -#} {% macro actions(id, principale, autres, reservee_au_menu, titre="cette offre", etiquette="", cible="") -%} {% set zone = cible or "#carte-" ~ id %} {% set total = (autres | length) + (1 if principale else 0) + (1 if reservee_au_menu else 0) %} {% set dans_menu = (autres + [reservee_au_menu]) if reservee_au_menu else autres %} {% set menu = dans_menu if total >= 3 else ([reservee_au_menu] if reservee_au_menu else []) %} {% set directes = [] if total >= 3 else autres %}
{% if etiquette %}{{ etiquette }}{% endif %} {% if principale %}{{ _action(principale, zone, principale=True) }}{% endif %} {% for a in directes %}{{ _action(a, zone) }}{% endfor %} {% if menu %} {% endif %}
{%- endmacro %} {#- Les actions d'une offre et ce qu'il faut dire dessous : pièces manquantes, échec, cause d'indisponibilité de l'IA, et les zones d'erreur `#etat-generation-` et `#etat-carte-` (cibles des erreurs des routes de génération et de statut). `acts` : un `contexte.Actions`. Rend la carte et l'en-tête de la fiche. -#} {% macro bloc_actions(id, titre, acts, cible="") -%} {{ actions(id, acts.principale, acts.autres, acts.reservee_au_menu, titre=titre, etiquette=acts.etiquette, cible=cible) }} {% if acts.manquantes %}

Pièces manquantes : {{ acts.manquantes | join(", ") }}.

{% endif %} {% if acts.message %}

{{ acts.message }}

{% endif %} {% if acts.cause and acts.principale and acts.principale.desactive %}

{{ acts.cause }}

{% endif %} {% if acts.lien_cv %}

Il manque un CV : créez-le dans Mon CV, puis utilisez « Utiliser mon CV de base » dans l'onglet CV adapté.

{% endif %}
{%- endmacro %} {#- Zone d'actions de l'en-tête de la fiche : `#generation-`. Tant qu'une préparation tourne (`en_cours`), elle se sonde toute seule (`every 3s`, `GET /offre//etat`) et se remplace (`outerHTML`). Rendue par `offre_base.html` et, seule, par la route `/etat`. `entete` : `Actions` de `contexte_offre` ; `titre` : nom de l'offre. -#} {% macro actions_fiche(id, entete, en_cours, titre="cette offre") -%}
{{ bloc_actions(id, titre, entete, cible="#generation-" ~ id) }}
{%- endmacro %} {#- L'action de préparation d'une offre (Préparer, Compléter, Préparation en cours…), à poser dans l'état vide d'un onglet (« Pas encore de lettre pour cette offre. »). `etat` : un `contexte.Actions` (celui de l'en-tête) ; ne rend rien si l'offre n'a plus de préparation à proposer. -#} {% macro action_principale(etat, id) -%} {% set p = etat.principale %} {% if p and p.cle in ("preparer", "completer", "en_cours") %}
{{ _action(p, "#generation-" ~ id, principale=True) }} {% if etat.cause and p.desactive %}

{{ etat.cause }}

{% endif %} {% if etat.message %}

{{ etat.message }}

{% endif %} {% if etat.manquantes %}

Pièces manquantes : {{ etat.manquantes | join(", ") }}.

{% endif %}
{% endif %} {%- endmacro %} {#- Message éphémère (spec § 1.4), échangé hors bande dans `#toasts` : à mettre dans la réponse d'un POST. `action` : None, ou {libelle, url, methode, vals, cible, echange} (`methode` "post" : formulaire htmx, par exemple « Annuler » qui reposte l'ancien statut ; "lien" : lien, par exemple « Voir »). Disparaît après 6 s (l'îlot arrête le minuteur tant que le toast a le focus ou le survol). -#} {% macro toast(texte, action=None) -%}
{{ texte }} {% if action and action.get("methode") == "lien" %} {{ action.libelle }} {% elif action %} {% endif %}
{%- endmacro %} {#- Ligne d'avancement d'une carte : `etapes` = [{libelle, etat}], `etat` ∈ "fait" (✓), "en_cours" (…), "a_faire" (○). Le symbole est caché aux lecteurs d'écran, qui lisent « Lettre : fait ». -#} {% macro avancement(etapes) -%}
    {% for e in etapes %} {% set mot = {"fait": "fait", "en_cours": "en cours", "a_faire": "à faire"}[e.etat] %}
  1. {{ e.libelle }}
  2. {% endfor %}
{%- endmacro %} {#- Motifs du score en pastilles : « python +4 » (`motifs` = [{terme, points}]). -#} {% macro pastilles_motifs(motifs) -%} {% if motifs %}
    {% for m in motifs %}
  • {{ m.terme }}{% if m.points %} {{ "+" if m.points > 0 else "" }}{{ m.points }}{% endif %}
  • {% endfor %}
{% endif %} {%- endmacro %} {#- Onglets d'une page (liens, `aria-current="page"`). `onglets` : [{cle, libelle, href, pret, aria_label}] (`pret` vrai ✓, faux ○, None sans symbole) ; `courant` : cle de l'onglet affiché ; `nom` : nom de la barre pour les lecteurs d'écran. -#} {% macro onglets_page(onglets, courant, nom="Sections de l'offre") -%} {%- endmacro %}