189 lines
8.0 KiB
Python
189 lines
8.0 KiB
Python
#!/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> </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()
|