#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ wireframes.py — Vignettes SVG des layouts (Sliding Design System) ================================================================= Schémas grosse maille, aux couleurs de la charte, FIDÈLES à la structure de chaque _render_* du moteur (cartes, bandeaux navy/coral, cercles, colonnes, barres, badges…). Pur Python/SVG : aucune dépendance, aucun LibreOffice, régénérable en une fraction de seconde. API : svg_for(layout_name, mode) -> str (SVG inline, viewBox 320x180). Un layout inconnu retombe sur un gabarit générique titre + lignes. Consommé par build_gallery.py (vignette dans chaque fiche). """ # ── Charte ─────────────────────────────────────────────────────────────────── NAVY = "#061033" NAVY2 = "#10204D" CORAL = "#F4795B" GLACIER = "#8FA9D0" SLATE = "#46505A" CARD = "#F4F1EC" BODY = "#2B3440" MUTED = "#C9CDD3" WHITE = "#FFFFFF" W, H = 320, 180 MX = 20 # marge horizontale de la zone utile # ── Primitives ─────────────────────────────────────────────────────────────── def _r(x, y, w, h, fill, rad=0, op=1.0, stroke=None): s = f' stroke="{stroke}" stroke-width="1"' if stroke else "" return (f'') def _c(cx, cy, rad, fill, op=1.0): return (f'') def _title_bar(y=16, w=150, c=NAVY): return _r(MX, y, w, 12, c, rad=2) def _lines(x, y, w, n, gap=13, c=MUTED, first=None, fw=None): out = [] for i in range(n): cc = first if (first and i == 0) else c ww = (fw or w) if (fw and i == 0) else w out.append(_r(x, y + i * gap, ww, 7, cc, rad=3)) return "".join(out) def _img_zone(x, y, w, h, mode="light"): """Zone image : aplat glacier clair + diagonale (convention wireframe).""" fill = GLACIER return (_r(x, y, w, h, fill, rad=4, op=0.5) + f'' + f'') def _frame(mode, body): bg = NAVY if mode == "dark" else ("#EEF1F6" if mode == "panel" else WHITE) return (f'' f'{_r(0, 0, W, H, bg, rad=8)}' f'{body}') def _title_light(mode): return WHITE if mode == "dark" else NAVY # ── Archétypes (fidèles aux _render_*) ─────────────────────────────────────── def _wf_cover(mode): # cover_split : fond navy, cercles décoratifs coin, filet coral, titre b = [_c(300, 20, 46, NAVY2), _c(288, 150, 30, CORAL, 0.9), _c(250, 150, 14, GLACIER), _r(MX, 74, 40, 4, CORAL, rad=1), _r(MX, 88, 150, 16, WHITE, rad=2), _r(MX, 120, 96, 8, GLACIER, rad=2)] return _frame("dark", "".join(b)) def _wf_section(mode): # section_divider : navy, gros numéro fantôme, titre b = [_c(300, 150, 40, NAVY2), _r(MX, 78, 20, 20, CORAL, rad=2), _r(MX, 104, 130, 14, WHITE, rad=2)] return _frame("dark", "".join(b)) def _wf_end(mode): b = [_c(300, 24, 40, CORAL, 0.9), _c(30, 150, 46, NAVY2), _r(96, 74, 128, 14, WHITE, rad=2), _r(120, 100, 80, 8, GLACIER, rad=2)] return _frame("dark", "".join(b)) def _wf_big_stat(mode): tl = _title_light(mode) # titre en haut ; chiffre coral CENTRÉ (H et V) ; desc + source # centrées dessous — cf _render_big_stat (align CENTER, _cy). b = [_title_bar(w=120, c=tl), _r(90, 66, 140, 40, CORAL, rad=4), # chiffre héros, centré _r(110, 116, 100, 8, MUTED, rad=3), # description centrée _r(134, 132, 52, 5, MUTED, rad=2, op=0.6)] # source centrée return _frame(mode, "".join(b)) def _wf_key_message(mode): # key_message : fond navy, grande phrase centrée b = [_r(40, 70, 240, 14, WHITE, rad=2), _r(70, 92, 180, 14, GLACIER, rad=2), _r(130, 120, 60, 6, CORAL, rad=2)] return _frame("dark", "".join(b)) def _wf_bullets(mode): tl = _title_light(mode) # titre en haut ; puces CARRÉES coral + texte, bloc centré vertical. # Pas de barre latérale (cf _render_default_bullets). b = [_title_bar(c=tl)] for y in (66, 94, 122): b.append(_r(MX, y, 8, 8, CORAL, rad=1)) # square_mark coral b.append(_r(MX + 18, y, 250, 8, MUTED, rad=3)) return _frame(mode, "".join(b)) def _wf_two_cols(mode): b = [_title_bar(w=170)] cw, xs, acc = 132, (MX, 168), (NAVY, CORAL) for i in range(2): x = xs[i] b.append(_r(x, 44, cw, 120, CARD, rad=6)) b.append(_r(x, 44, cw, 26, acc[i], rad=6)) b.append(_r(x, 58, cw, 12, acc[i])) b.append(_r(x + 12, 52, 70, 10, WHITE, rad=3)) b.append(_lines(x + 12, 84, 100, 4, gap=15)) return _frame("light", "".join(b)) def _wf_from_to(mode): b = [_title_bar(w=150)] for i, (x, c) in enumerate(((MX, NAVY), (176, SLATE))): b.append(_r(x, 50, 120, 100, CARD, rad=6)) b.append(_r(x, 50, 120, 6, c, rad=3)) b.append(_lines(x + 12, 70, 90, 4, gap=16)) b.append(_c(160, 100, 10, CORAL)) # flèche/pivot central return _frame("light", "".join(b)) def _wf_exec_summary(mode): b = [_title_bar(w=160)] for i, x in enumerate((MX, 112, 204)): b.append(_r(x, 54, 96, 108, CARD, rad=6)) b.append(_c(x + 16, 74, 9, NAVY)) b.append(_r(x + 12, 96, 72, 7, SLATE, rad=3)) b.append(_lines(x + 12, 112, 72, 3, gap=12)) b.append(_r(MX, 54, 96, 4, CORAL, rad=2)) # 1re carte accentuée return _frame("light", "".join(b)) def _wf_kpi_grid(mode): b = [_title_bar(w=120)] xs = (MX, 112, 204) for i, x in enumerate(xs): b.append(_r(x, 56, 96, 100, CARD, rad=6)) b.append(_r(x + 12, 74, 50, 22, CORAL if i == 0 else NAVY, rad=3)) b.append(_r(x + 12, 104, 70, 6, SLATE, rad=3)) b.append(_r(x + 12, 118, 56, 6, MUTED, rad=3)) return _frame("light", "".join(b)) def _wf_numbered_steps(mode): b = [_title_bar(w=150)] for i, y in enumerate((52, 86, 120)): b.append(_r(MX, y, 280, 28, CARD, rad=6)) b.append(_c(MX + 18, y + 14, 11, NAVY)) b.append(_r(MX + 40, y + 10, 200, 8, MUTED, rad=3)) return _frame("light", "".join(b)) def _wf_agenda(mode): b = [_title_bar(w=140)] for i, y in enumerate((52, 84, 116, 148)): active = (i == 1) b.append(_c(MX + 12, y, 10, CORAL if active else NAVY)) b.append(_r(MX + 30, y - 5, 180, 9, NAVY if active else MUTED, rad=3)) b.append(_r(230, y - 5, 60, 9, MUTED, rad=3, op=0.6)) return _frame("light", "".join(b)) def _wf_circular(mode): tl = _title_light(mode) b = [_title_bar(w=130, c=tl)] cx, cy = 110, 108 for ang, c in ((0, NAVY), (120, SLATE), (240, GLACIER)): import math x = cx + 34 * math.cos(math.radians(ang)) y = cy + 34 * math.sin(math.radians(ang)) b.append(_c(x, y, 22, c)) b.append(_c(cx, cy, 14, CORAL)) # légende for i, y in enumerate((70, 100, 130)): b.append(_c(210, y, 6, (NAVY, SLATE, GLACIER)[i])) b.append(_r(224, y - 4, 76, 7, MUTED, rad=3)) return _frame(mode, "".join(b)) def _wf_recommendation(mode): b = [_title_bar(w=150), _r(MX, 48, 190, 116, CARD, rad=6), _r(MX, 48, 190, 6, CORAL, rad=3), _c(MX + 20, 72, 10, NAVY), _lines(MX + 40, 66, 130, 4, gap=16), _r(228, 48, 72, 116, NAVY, rad=6), # panneau latéral navy _r(240, 70, 48, 8, GLACIER, rad=3), _lines(240, 92, 48, 3, gap=13, c="#33436A")] return _frame("light", "".join(b)) def _wf_phases_timeline(mode): b = [_title_bar(w=150)] for i, x in enumerate((MX, 112, 204)): c = (NAVY, SLATE, GLACIER)[i] b.append(_r(x, 74, 96, 44, c, rad=6)) if i < 2: b.append(_r(x + 96, 94, 16, 4, MUTED, rad=2)) b.append(_r(x + 20, 126, 56, 6, MUTED, rad=3)) return _frame("light", "".join(b)) def _wf_gantt(mode): b = [_title_bar(w=140)] rows = ((40, 120, NAVY), (90, 90, SLATE), (150, 100, NAVY), (70, 130, GLACIER)) for i, (x, w, c) in enumerate(rows): y = 56 + i * 26 b.append(_r(MX, y, 60, 10, MUTED, rad=2)) # label b.append(_r(90 + x, y, w, 12, c, rad=3)) # barre return _frame("light", "".join(b)) def _wf_yearly(mode): b = [_title_bar(w=130), _r(MX, 100, 280, 3, NAVY)] # ligne de temps for i, x in enumerate((40, 120, 200, 280)): b.append(_c(x, 101, 7, CORAL if i == 1 else NAVY)) b.append(_r(x - 20, 116, 40, 6, MUTED, rad=2)) return _frame("light", "".join(b)) def _wf_comparison(mode): b = [_title_bar(w=150), _r(MX, 48, 280, 20, NAVY, rad=4)] # entête for j in range(3): b.append(_r(MX + 4 + j * 92, 52, 84, 12, WHITE, rad=2, op=0.5)) for i in range(4): y = 72 + i * 22 b.append(_r(MX, y, 280, 18, CARD if i % 2 else WHITE, rad=2, stroke="#E3E0D9")) for j in range(3): b.append(_r(MX + 10 + j * 92, y + 5, 64, 7, MUTED, rad=2)) return _frame("light", "".join(b)) def _wf_raci(mode): b = [_title_bar(w=120), _r(MX, 48, 280, 18, NAVY, rad=4)] letters = (NAVY, CORAL, SLATE, GLACIER) for i in range(4): y = 70 + i * 22 b.append(_r(MX, y, 80, 16, CARD, rad=2)) for j in range(4): b.append(_c(130 + j * 44, y + 8, 7, letters[j], 0.85)) return _frame("light", "".join(b)) def _wf_process_arrow(mode): b = [_title_bar(w=140)] for i, x in enumerate((MX, 120, 220)): c = CORAL if i == 1 else NAVY b.append(f'') return _frame("light", "".join(b)) def _wf_org_chart(mode): b = [_title_bar(w=120), _r(130, 52, 60, 26, NAVY, rad=4)] for x in (60, 140, 220): b.append(_r(x, 120, 56, 26, CARD, rad=4, stroke="#E3E0D9")) b.append(f'') return _frame("light", "".join(b)) def _wf_matrix(mode): tl = _title_light(mode) b = [_title_bar(w=120, c=tl), f'', f''] quad = ((92, 78, NAVY), (228, 78, CORAL), (92, 140, SLATE), (228, 140, GLACIER)) for cx, cy, c in quad: b.append(_c(cx, cy, 16, c, 0.9)) return _frame(mode, "".join(b)) def _wf_image_split(mode): b = [_img_zone(MX, 44, 130, 120), _r(168, 44, 130, 14, NAVY, rad=2), _c(176, 78, 3, CORAL), _lines(184, 74, 108, 4, gap=17)] return _frame("light", "".join(b)) def _wf_image_full(mode): b = [_img_zone(0, 0, W, H), _r(0, 120, W, 60, NAVY, rad=0, op=0.82), _r(MX, 134, 150, 12, WHITE, rad=2), _r(MX, 152, 90, 7, GLACIER, rad=2)] return _frame("light", "".join(b)) def _wf_bar_chart(mode): b = [_title_bar(w=120)] vals = (40, 70, 55, 90, 62) for i, v in enumerate(vals): x = 40 + i * 52 b.append(_r(x, 150 - v, 32, v, NAVY, rad=2)) b.append(_r(30, 150, 270, 2, SLATE)) return _frame("light", "".join(b)) def _wf_line_chart(mode): b = [_title_bar(w=120), _r(30, 150, 270, 2, SLATE)] pts = [(40, 130), (100, 96), (160, 110), (220, 70), (288, 84)] path = " ".join(f"{x},{y}" for x, y in pts) b.append(f'') b.append(_c(288, 84, 5, CORAL)) return _frame("light", "".join(b)) def _wf_donut(mode): tl = _title_light(mode) b = [_title_bar(w=110, c=tl)] b.append(f'') b.append(f'') for i, y in enumerate((84, 108, 132)): b.append(_c(210, y, 6, (NAVY, CORAL, GLACIER)[i])) b.append(_r(224, y - 4, 76, 7, MUTED, rad=3)) return _frame(mode, "".join(b)) def _wf_waterfall(mode): b = [_title_bar(w=140), _r(30, 150, 270, 2, SLATE)] # colonnes cumulées : base navy, deltas coral, total navy cols = ((40, 150 - 70, 70, NAVY), (94, 80, 34, CORAL), (148, 60, 40, CORAL), (202, 44, 30, CORAL), (256, 150 - 90, 90, NAVY)) for x, y, h, c in cols: b.append(_r(x, y, 34, h, c, rad=2)) return _frame("light", "".join(b)) def _wf_heatmap(mode): b = [_title_bar(w=120)] import random random.seed(3) shades = ["#D6DCEA", "#9FB0D0", GLACIER, NAVY2, NAVY] for r in range(4): for cix in range(5): b.append(_r(60 + cix * 46, 54 + r * 26, 42, 22, shades[(r + cix) % 5], rad=2)) b.append(_r(MX, 58 + r * 26, 32, 12, MUTED, rad=2)) return _frame("light", "".join(b)) def _wf_funnel(mode): b = [_title_bar(w=110)] widths = (240, 180, 120, 70) cols = (NAVY, NAVY2, GLACIER, CORAL) for i, w in enumerate(widths): x = (W - w) / 2 b.append(f'') return _frame("light", "".join(b)) def _wf_pyramid(mode): b = [_title_bar(w=110)] widths = (90, 150, 210, 270) cols = (CORAL, GLACIER, SLATE, NAVY) for i, w in enumerate(widths): x = (W - w) / 2 b.append(_r(x, 132 - i * 24, w, 20, cols[i], rad=2)) return _frame("light", "".join(b)) def _wf_generic(mode): tl = _title_light(mode) return _frame(mode, _title_bar(c=tl) + _lines(MX, 50, 240, 5, gap=18)) # ── Mapping layout → archétype ─────────────────────────────────────────────── _MAP = { "cover_split": _wf_cover, "section_divider": _wf_section, "end_slide": _wf_end, "big_stat": _wf_big_stat, "key_message": _wf_key_message, "default_bullets": _wf_bullets, "two_cols_text": _wf_two_cols, "from_to_pairs": _wf_from_to, "executive_summary": _wf_exec_summary, "kpi_grid": _wf_kpi_grid, "numbered_steps": _wf_numbered_steps, "agenda": _wf_agenda, "circular_diagram": _wf_circular, "recommendation_card": _wf_recommendation, "phases_timeline": _wf_phases_timeline, "gantt_timeline": _wf_gantt, "yearly_timeline": _wf_yearly, "comparison_table": _wf_comparison, "raci_table": _wf_raci, "process_arrow": _wf_process_arrow, "org_chart": _wf_org_chart, "matrix_2x2": _wf_matrix, "image_split": _wf_image_split, "image_full": _wf_image_full, "bar_chart": _wf_bar_chart, "line_chart": _wf_line_chart, "donut_split": _wf_donut, "waterfall": _wf_waterfall, "heatmap_table": _wf_heatmap, "funnel": _wf_funnel, "pyramid": _wf_pyramid, # freeform : pas de structure fixe → gabarit générique "freeform": _wf_generic, } def svg_for(layout_name, mode="light"): fn = _MAP.get(layout_name, _wf_generic) try: return fn(mode) except Exception: return _wf_generic(mode) def known(): return set(_MAP) if __name__ == "__main__": # Auto-test : rend tous les archétypes dans un HTML de contrôle import yaml import os try: L = yaml.safe_load(open("layouts_v2.yaml", encoding="utf-8"))["layouts"] names = list(L) except Exception: names = list(_MAP) cells = "".join( f'
' f'{svg_for(n)}
{n}
' for n in names) open("_wireframes_preview.html", "w").write( f"{cells}") missing = [n for n in names if n not in _MAP] print("layouts:", len(names), "| mappés:", len([n for n in names if n in _MAP]), "| génériques:", missing or "aucun") print("Contrôle visuel : _wireframes_preview.html")