supersam/docs/autocall/render-html.py

189 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""HTML-версия презентации «Автодозвон: как это работает» (тот же текст, что в PDF).
Содержание берётся из deck_content.py, поэтому PDF и HTML не разъезжаются.
Файл самодостаточный: без внешних шрифтов, картинок и скриптов — открывается
двойным щелчком и с телефона, и печатается в PDF по одному слайду на страницу.
Запуск: python3 docs/autocall/render-html.py
"""
from html import escape
from pathlib import Path
import deck_content as content
OUT = Path(__file__).with_name("autocall-how-it-works.html")
TONE = {
"accent": "#4ADE80",
"accent2": "#60A5FA",
"warn": "#FBBF24",
"danger": "#F87171",
}
def esc(text):
return escape(str(text))
def card_html(card, extra_class=""):
tone = TONE.get(card.get("accent", "accent"), TONE["accent"])
items = "".join(f"<li>{esc(i)}</li>" for i in card["items"])
classes = " ".join(f"card {extra_class} {'dense' if card.get('dense') else ''}".split())
return (
f'<section class="{classes}" style="--tone:{tone}">'
f'<h3>{esc(card["title"])}</h3><ul>{items}</ul></section>'
)
def table_html(table):
head, *body = table["rows"]
widths = table.get("widths") or []
colgroup = "".join(f'<col style="width:{w / sum(widths) * 100:.2f}%">' for w in widths) if widths else ""
head_html = "".join(f"<th>{esc(c)}</th>" for c in head)
body_html = "".join(
"<tr>" + "".join(f"<td>{esc(c)}</td>" for c in row) + "</tr>" for row in body
)
return (
f'<table>{"<colgroup>" + colgroup + "</colgroup>" if colgroup else ""}'
f"<thead><tr>{head_html}</tr></thead><tbody>{body_html}</tbody></table>"
)
def steps_html(steps):
out = []
for i, (title, desc) in enumerate(steps):
lines = "".join(f"<span>{esc(line)}</span>" for line in desc.split("\n"))
arrow = '<div class="arrow">›</div>' if i < len(steps) - 1 else ""
out.append(f'<div class="step"><b>{esc(title)}</b>{lines}{arrow}</div>')
return f'<div class="steps">{"".join(out)}</div>'
def slide_html(slide, number):
kind = slide["kind"]
parts = ['<footer><span>Автодозвон SuperSam · как это работает</span>'
f'<span>{number}</span></footer>']
if kind == "title":
lines = "".join(f"<p>{esc(line)}</p>" if line else "<p>&nbsp;</p>" for line in slide["lines"])
body = (
f'<div class="kicker">{esc(slide["kicker"])}</div>'
f'<h1>{esc(slide["title"])}</h1>'
f'<p class="sub">{esc(slide["sub"])}</p>'
f'<div class="card titlecard" style="--tone:{TONE["accent"]}">{lines}</div>'
)
else:
head = f'<h2>{esc(slide["title"])}</h2><p class="sub">{esc(slide["sub"])}</p>'
body = head
if kind == "steps":
body += steps_html(slide["steps"])
elif kind in ("cards2full", "cards3full"):
cards = "".join(card_html(c) for c in slide["cards"])
body += f'<div class="grid {kind}">{cards}</div>'
elif kind == "tablecard":
body += table_html(slide["table"])
full = slide.get("full")
if full:
body += card_html(full, extra_class="full")
return f'<section class="slide {kind}">{body}{parts[0]}</section>'
def build():
slides = "".join(slide_html(s, i + 1) for i, s in enumerate(content.ALL_SLIDES))
html = f"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Автодозвон SuperSam — как это работает</title>
<style>
:root {{
--bg: #0B1220; --bg2: #101A2C; --card: #131E33; --border: #25344F;
--text: #E8EEF9; --dim: #9FB0CC; --accent: {TONE['accent']};
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0; background: var(--bg); color: var(--text);
font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex; flex-direction: column; align-items: center; gap: 24px; padding: 24px 12px 48px;
}}
.slide {{
position: relative; width: min(1180px, 100%); aspect-ratio: 16 / 9; overflow: hidden;
background: linear-gradient(180deg, var(--bg2) 0%, var(--bg) 34%);
border: 1px solid var(--border); border-radius: 18px;
padding: 36px 40px 46px; display: flex; flex-direction: column; gap: 14px;
}}
.kicker {{ color: var(--accent); font-weight: 700; letter-spacing: .08em; font-size: 13px; text-transform: uppercase; }}
h1 {{ font-size: 40px; line-height: 1.15; margin: 6px 0 0; }}
h2 {{ font-size: 26px; margin: 0; }}
h3 {{ color: var(--tone, var(--accent)); font-size: 15px; margin: 0 0 8px; }}
.sub {{ color: var(--dim); margin: 4px 0 6px; font-size: 15px; }}
.grid {{ display: grid; gap: 14px; }}
.grid.cards2full {{ grid-template-columns: 1fr 1fr; }}
.grid.cards3full {{ grid-template-columns: repeat(3, 1fr); }}
.card {{
background: var(--card); border: 1px solid var(--border); border-left: 3px solid var(--tone, var(--accent));
border-radius: 12px; padding: 14px 16px;
}}
.card.full {{ grid-column: 1 / -1; }}
.card ul {{ margin: 0; padding-left: 18px; }}
.card li {{ margin: 5px 0; font-size: 14.5px; color: var(--text); }}
.card li::marker {{ color: var(--tone, var(--accent)); }}
.card.dense ul {{ column-count: 2; column-gap: 28px; }}
.titlecard p {{ margin: 6px 0; color: var(--text); }}
.steps {{ display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }}
.step {{
position: relative; background: var(--card); border: 1px solid var(--border); border-radius: 12px;
padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;
}}
.step b {{ color: var(--accent); font-size: 13.5px; }}
.step span {{ color: var(--text); font-size: 13px; }}
.arrow {{ position: absolute; right: -11px; top: 50%; transform: translateY(-50%); color: var(--dim); font-size: 20px; }}
table {{ width: 100%; border-collapse: collapse; background: var(--card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }}
thead th {{ background: #182640; color: var(--text); text-align: left; font-size: 13.5px; }}
th, td {{ padding: 9px 12px; border-bottom: 1px solid var(--border); font-size: 13.5px; vertical-align: top; }}
tbody tr:nth-child(even) td {{ background: rgba(255,255,255,.015); }}
th:first-child, td:first-child {{ color: var(--dim); }}
footer {{
position: absolute; left: 40px; right: 40px; bottom: 14px; display: flex; justify-content: space-between;
color: var(--dim); font-size: 11.5px; border-top: 1px solid var(--border); padding-top: 8px;
}}
@media (max-width: 1200px) {{
/* Узкое окно: фиксированная 16:9 начинает резать текст — даём слайду расти */
.slide {{ aspect-ratio: auto; }}
}}
@media (max-width: 900px) {{
.grid.cards2full, .grid.cards3full, .steps {{ grid-template-columns: 1fr; }}
.card.dense ul {{ column-count: 1; }}
.arrow {{ display: none; }}
h2 {{ font-size: 22px; }}
}}
@media (max-width: 760px) {{
body {{ padding: 12px 8px 32px; }}
.slide {{ aspect-ratio: auto; padding: 22px 18px 52px; }}
h1 {{ font-size: 26px; }} h2 {{ font-size: 20px; }}
.grid.cards2full, .grid.cards3full, .steps {{ grid-template-columns: 1fr; }}
.card.dense ul {{ column-count: 1; }}
.arrow {{ display: none; }}
th, td {{ font-size: 12px; padding: 7px 8px; }}
.card li {{ font-size: 13.5px; }}
}}
@media print {{
body {{ background: #fff; padding: 0; gap: 0; }}
.slide {{ width: 100%; height: 100vh; border-radius: 0; border: 0; page-break-after: always; }}
}}
</style>
</head>
<body>
{slides}
</body>
</html>
"""
OUT.write_text(html, encoding="utf-8")
print(f"готово: {OUT} — слайдов {len(content.ALL_SLIDES)}")
if __name__ == "__main__":
build()