"""
ui_components.py — HTML card builders for each legal source type + tab panel.
Doctrine.fr-inspired design: clean white cards, subtle borders, navy accents.
"""
# ── Shared card wrapper ──
_CARD_STYLE = (
"border:1px solid #e2e8f0;border-radius:10px;padding:16px 20px;"
"margin-bottom:12px;background:#ffffff;"
"box-shadow:0 1px 3px rgba(0,0,0,0.04);"
"transition:box-shadow 0.15s,border-color 0.15s"
)
# ── Badge colors per source ──
_BADGE = {
"article": ("background:#dbeafe;color:#1e40af", "Code"),
"decision": ("background:#fce7f3;color:#9d174d", "Jurisprudence"),
"circulaire": ("background:#d1fae5;color:#065f46", "Circulaire"),
"reponse": ("background:#fef3c7;color:#92400e", "Q&R"),
}
def _badge(style: str, text: str) -> str:
return (
f'{text}'
)
def _meta_line(*parts: str) -> str:
items = [p for p in parts if p]
return (
'
'
+ " · ".join(items)
+ '
'
)
def build_article_card(result: dict, related_decisions: list[dict] | None = None) -> str:
code = result.get("code_name", "Code")
num = result.get("num", result.get("id_legifrance", ""))
snippet = (result.get("chunk_text") or "")[:250]
lf_id = result.get("id_legifrance", "")
url = f"https://www.legifrance.gouv.fr/codes/article_lc/{lf_id}" if lf_id else "#"
etat = result.get("article_etat", "")
etat_html = ""
if etat:
color = "#059669" if etat == "VIGUEUR" else "#94a3b8"
etat_html = f'{etat}'
cross_ref_html = ""
if related_decisions:
mini_cards = ""
for dec in related_decisions:
dec_url = dec.get("url_judilibre", "#")
dec_date = dec.get("date_decision", "")
dec_jur = dec.get("jurisdiction", "")
dec_snip = dec.get("chunk_text", "")[:120]
mini_cards += f"""
"""
n = len(related_decisions)
cross_ref_html = f"""
{n} decision{"s" if n > 1 else ""} liee{"s" if n > 1 else ""}
{mini_cards}
"""
return f"""
{_badge(*_BADGE["article"][:1], code)}
Art. {num}
{etat_html}
{snippet}…
{_meta_line(f'
Legifrance →')}
{cross_ref_html}
"""
def build_decision_card(result: dict) -> str:
juris = result.get("jurisdiction", "")
chamber = result.get("chamber", "")
date = result.get("date_decision", "")
fiche = result.get("fiche_arret") or ""
snippet = fiche[:250] if fiche else (result.get("chunk_text") or "")[:250]
url = result.get("url_judilibre", "#")
src_id = result.get("source_id", "")
label = juris + (f" · {chamber}" if chamber else "")
return f"""
{_badge(_BADGE["decision"][0], label)}
{date}
{f'n° {src_id}' if src_id else ""}
{snippet}…
{_meta_line(f'
Cour de cassation →')}
"""
def build_circulaire_card(result: dict) -> str:
ministere = result.get("ministere", "")
numero = result.get("numero", result.get("source_id", ""))
objet = (result.get("objet") or result.get("chunk_text") or "")[:250]
date = (result.get("date_parution") or "")[:10]
url = result.get("url_legifrance", "#")
return f"""
{_badge(_BADGE["circulaire"][0], f"Min. {ministere}")}
Circ. n° {numero}
{objet}…
{_meta_line(date, f'
Legifrance →')}
"""
def build_reponse_card(result: dict) -> str:
ministere = result.get("ministere", "")
num_q = result.get("numero_question", result.get("source_id", ""))
question = (result.get("question_text") or result.get("chunk_text") or "")[:250]
date = (result.get("date_reponse") or "")[:10]
url = result.get("url_legifrance", "#")
return f"""
{_badge(_BADGE["reponse"][0], ministere)}
Q. n° {num_q}
{question}…
{_meta_line(date, f'
Legifrance →')}
"""
def build_tabs_html(results_dict: dict, loading_status: dict) -> str:
tabs_config = [
("articles", "Articles", build_article_card),
("jurisprudence", "Jurisprudence", build_decision_card),
("circulaires", "Circulaires", build_circulaire_card),
("reponses", "Q&R", build_reponse_card),
]
tab_buttons = ""
tab_panels = ""
for i, (key, label, builder) in enumerate(tabs_config):
results = results_dict.get(key, [])
count = len(results)
# Active tab styling
if i == 0:
bg, color, weight, border = "#e8f0fe", "#1e3a5f", "600", "2px solid #1e3a5f"
else:
bg, color, weight, border = "transparent", "#64748b", "400", "2px solid transparent"
tab_buttons += f"""
"""
if not loading_status.get(key, False):
content = 'Source temporairement indisponible
'
elif not results:
content = 'Aucun resultat pour cette source.
'
else:
content = "".join(builder(r) for r in results)
display = "block" if i == 0 else "none"
tab_panels += f"""
{content}
"""
js = """
"""
return f"""
{tab_buttons}
{tab_panels}
{js}
"""