@nguyenphp/antigravity-marketing 1.0.18 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -78
- package/package.json +4 -3
- package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
- package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
- package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
- package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
- package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
- package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
- package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
- package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
- package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
- package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
- package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
- package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
- package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
- package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
- package/templates/.agent/skills/minimax-pdf/README.md +222 -0
- package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
- package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
- package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
- package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
- package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
- package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
- package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
- package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
- package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
- package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
- package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
- package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
- package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
- package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
- package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
- package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
- package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
- package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
- package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
- package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
- package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
- package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
- package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +170 -585
- package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
- package/templates/.agent/workflows/analyze.md +3 -0
- package/templates/.agent/workflows/brand-report.md +44 -0
- package/templates/.agent/workflows/report.md +49 -0
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
cover.py — Generate cover.html from tokens.json.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 cover.py --tokens tokens.json --out cover.html
|
|
7
|
+
|
|
8
|
+
Reads tokens.json["cover_pattern"] and renders the matching HTML cover.
|
|
9
|
+
Cover fonts are loaded live via Google Fonts @import (no local caching).
|
|
10
|
+
Exit codes: 0 success, 1 bad args/missing file, 3 render error
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── Google Fonts loader ────────────────────────────────────────────────────────
|
|
19
|
+
def _gfonts_import(t: dict) -> str:
|
|
20
|
+
"""Return a CSS @import for the document's Google Fonts, if available."""
|
|
21
|
+
url = t.get("gfonts_import", "")
|
|
22
|
+
if url:
|
|
23
|
+
return f"@import url('{url}');"
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── Shared CSS head (required by all patterns) ─────────────────────────────────
|
|
28
|
+
def _base_css(t: dict) -> str:
|
|
29
|
+
"""Critical reset + shared variables. Never remove these rules."""
|
|
30
|
+
return f"""
|
|
31
|
+
{_gfonts_import(t)}
|
|
32
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
33
|
+
html, body {{
|
|
34
|
+
width: 794px; height: 1123px;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
background: {t['cover_bg']};
|
|
37
|
+
font-family: '{t['font_body']}', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
38
|
+
}}
|
|
39
|
+
.page {{
|
|
40
|
+
position: relative;
|
|
41
|
+
width: 794px; height: 1123px;
|
|
42
|
+
background: {t['cover_bg']};
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Dot-grid SVG helper ─────────────────────────────────────────────────────────
|
|
49
|
+
def _dot_grid(x0, y0, cols, rows, *, gap, r, color, opacity) -> str:
|
|
50
|
+
"""Render a dot-grid as an absolutely positioned SVG element."""
|
|
51
|
+
dots = []
|
|
52
|
+
for row in range(rows):
|
|
53
|
+
for col in range(cols):
|
|
54
|
+
cx = x0 + col * gap
|
|
55
|
+
cy = y0 + row * gap
|
|
56
|
+
dots.append(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{color}"/>')
|
|
57
|
+
return (
|
|
58
|
+
f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
|
|
59
|
+
f'pointer-events:none;opacity:{opacity}" xmlns="http://www.w3.org/2000/svg">'
|
|
60
|
+
+ "".join(dots) + "</svg>"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── Cross-hatch SVG helper ──────────────────────────────────────────────────────
|
|
65
|
+
def _cross_hatch(color, opacity, spacing=32, stroke_w=0.5) -> str:
|
|
66
|
+
lines = []
|
|
67
|
+
for i in range(-20, 60):
|
|
68
|
+
x = i * spacing
|
|
69
|
+
lines.append(f'<line x1="{x}" y1="0" x2="{x + 1200}" y2="1200" stroke="{color}" stroke-width="{stroke_w}"/>')
|
|
70
|
+
return (
|
|
71
|
+
f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
|
|
72
|
+
f'pointer-events:none;opacity:{opacity};overflow:hidden" xmlns="http://www.w3.org/2000/svg">'
|
|
73
|
+
+ "".join(lines) + "</svg>"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── Pattern 1: Full-bleed block ────────────────────────────────────────────────
|
|
78
|
+
def _pattern_fullbleed(t: dict) -> str:
|
|
79
|
+
dot_grid = _dot_grid(
|
|
80
|
+
x0=500, y0=40, cols=10, rows=20, gap=24, r=1.8,
|
|
81
|
+
color=t["accent"], opacity=0.12
|
|
82
|
+
)
|
|
83
|
+
subtitle_block = ""
|
|
84
|
+
if t.get("subtitle"):
|
|
85
|
+
subtitle_block = f"""
|
|
86
|
+
<div style="font-size:14px;color:{t['muted']};letter-spacing:0.01em;
|
|
87
|
+
max-width:480px;line-height:1.5;margin-bottom:40px;">
|
|
88
|
+
{t['subtitle']}
|
|
89
|
+
</div>"""
|
|
90
|
+
|
|
91
|
+
return f"""<!DOCTYPE html>
|
|
92
|
+
<html>
|
|
93
|
+
<head><meta charset="UTF-8">
|
|
94
|
+
<style>
|
|
95
|
+
{_base_css(t)}
|
|
96
|
+
.label {{
|
|
97
|
+
font-size: 9px; font-weight: 500; letter-spacing: 0.22em;
|
|
98
|
+
color: {t['accent']}; text-transform: uppercase; margin-bottom: 28px;
|
|
99
|
+
}}
|
|
100
|
+
.title {{
|
|
101
|
+
font-family: '{t['font_display']}', 'Times New Roman', Georgia, serif;
|
|
102
|
+
font-weight: 900; font-size: 60px; line-height: 1.0;
|
|
103
|
+
color: {t['text_light']}; letter-spacing: -0.015em;
|
|
104
|
+
margin-bottom: 10px; max-width: 560px;
|
|
105
|
+
word-wrap: break-word;
|
|
106
|
+
}}
|
|
107
|
+
.rule {{
|
|
108
|
+
width: 52%; height: 1.5px;
|
|
109
|
+
background: linear-gradient(to right, {t['accent']}, transparent);
|
|
110
|
+
margin: 24px 0 20px;
|
|
111
|
+
}}
|
|
112
|
+
.content {{
|
|
113
|
+
position: absolute; left: 68px; right: 60px;
|
|
114
|
+
top: 0; bottom: 0;
|
|
115
|
+
display: flex; flex-direction: column; justify-content: center;
|
|
116
|
+
padding-top: 60px;
|
|
117
|
+
}}
|
|
118
|
+
.footer {{
|
|
119
|
+
position: absolute; bottom: 0; left: 0; right: 0;
|
|
120
|
+
height: 70px;
|
|
121
|
+
background: rgba(0,0,0,0.22);
|
|
122
|
+
display: flex; align-items: center;
|
|
123
|
+
justify-content: space-between;
|
|
124
|
+
padding: 0 68px;
|
|
125
|
+
}}
|
|
126
|
+
.footer-author {{ font-size: 11px; color: rgba(240,237,230,0.75); letter-spacing:0.04em; }}
|
|
127
|
+
.footer-date {{ font-size: 11px; color: {t['muted']}; letter-spacing: 0.04em; }}
|
|
128
|
+
</style>
|
|
129
|
+
</head>
|
|
130
|
+
<body>
|
|
131
|
+
<div class="page">
|
|
132
|
+
<!-- top-right accent strip -->
|
|
133
|
+
<div style="position:absolute;top:0;right:0;width:35%;height:4px;background:{t['accent']};"></div>
|
|
134
|
+
<!-- left vertical accent bar (gradient fade) -->
|
|
135
|
+
<div style="position:absolute;left:48px;top:18%;width:3px;height:60%;
|
|
136
|
+
background:linear-gradient(to bottom,{t['accent']},transparent);"></div>
|
|
137
|
+
<!-- dot grid background texture -->
|
|
138
|
+
{dot_grid}
|
|
139
|
+
|
|
140
|
+
<div class="content">
|
|
141
|
+
<div class="label">{t.get('doc_type','Document').upper()} · {t.get('date','')}</div>
|
|
142
|
+
<div class="title">{t['title']}</div>
|
|
143
|
+
<div class="rule"></div>
|
|
144
|
+
{subtitle_block}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="footer">
|
|
148
|
+
<div class="footer-author">{t.get('author','')}</div>
|
|
149
|
+
<div class="footer-date">{t.get('date','')}</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</body></html>"""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Pattern 2: Split panel ─────────────────────────────────────────────────────
|
|
156
|
+
def _pattern_split(t: dict) -> str:
|
|
157
|
+
dot_grid = _dot_grid(
|
|
158
|
+
x0=360, y0=120, cols=10, rows=18, gap=22, r=2,
|
|
159
|
+
color="#CCCCCC", opacity=0.25
|
|
160
|
+
)
|
|
161
|
+
return f"""<!DOCTYPE html>
|
|
162
|
+
<html>
|
|
163
|
+
<head><meta charset="UTF-8">
|
|
164
|
+
<style>
|
|
165
|
+
{_base_css(t)}
|
|
166
|
+
.left-panel {{
|
|
167
|
+
position: absolute; top: 0; left: 0;
|
|
168
|
+
width: 330px; height: 1123px;
|
|
169
|
+
background: {t['cover_bg']};
|
|
170
|
+
display: flex; flex-direction: column;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
padding: 0 44px;
|
|
173
|
+
}}
|
|
174
|
+
.right-panel {{
|
|
175
|
+
position: absolute; top: 0; left: 330px;
|
|
176
|
+
width: 464px; height: 1123px;
|
|
177
|
+
background: {t['page_bg']};
|
|
178
|
+
}}
|
|
179
|
+
.divider {{
|
|
180
|
+
position: absolute; top: 0; left: 329px;
|
|
181
|
+
width: 3px; height: 1123px;
|
|
182
|
+
background: {t['accent']};
|
|
183
|
+
}}
|
|
184
|
+
.left-top-bar {{
|
|
185
|
+
position: absolute; top: 0; left: 0;
|
|
186
|
+
width: 330px; height: 4px;
|
|
187
|
+
background: {t['accent']};
|
|
188
|
+
}}
|
|
189
|
+
.title {{
|
|
190
|
+
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
|
191
|
+
font-weight: 900; font-size: 34px; line-height: 1.2;
|
|
192
|
+
color: {t['text_light']}; margin-bottom: 18px;
|
|
193
|
+
word-wrap: break-word;
|
|
194
|
+
}}
|
|
195
|
+
.rule {{
|
|
196
|
+
width: 55%; height: 1.5px;
|
|
197
|
+
background: {t['accent']};
|
|
198
|
+
margin-bottom: 14px;
|
|
199
|
+
}}
|
|
200
|
+
.subtitle {{
|
|
201
|
+
font-size: 12px; color: rgba(220,220,220,0.65);
|
|
202
|
+
line-height: 1.5; margin-bottom: 32px;
|
|
203
|
+
}}
|
|
204
|
+
.author {{
|
|
205
|
+
font-size: 11px; color: {t['text_light']}; margin-bottom: 4px;
|
|
206
|
+
}}
|
|
207
|
+
.date {{ font-size: 10px; color: {t['muted']}; }}
|
|
208
|
+
.right-label {{
|
|
209
|
+
position: absolute; bottom: 60px; right: 44px;
|
|
210
|
+
font-size: 9px; letter-spacing: 0.18em;
|
|
211
|
+
color: {t['muted']}; text-transform: uppercase;
|
|
212
|
+
}}
|
|
213
|
+
</style>
|
|
214
|
+
</head>
|
|
215
|
+
<body>
|
|
216
|
+
<div class="page">
|
|
217
|
+
<div class="left-top-bar"></div>
|
|
218
|
+
<div class="left-panel">
|
|
219
|
+
<div class="title">{t['title']}</div>
|
|
220
|
+
<div class="rule"></div>
|
|
221
|
+
{'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
|
|
222
|
+
<div class="author">{t.get('author','')}</div>
|
|
223
|
+
<div class="date">{t.get('date','')}</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="right-panel">
|
|
226
|
+
{dot_grid}
|
|
227
|
+
</div>
|
|
228
|
+
<div class="divider"></div>
|
|
229
|
+
<div class="right-label">{t.get('doc_type','').upper()}</div>
|
|
230
|
+
</div>
|
|
231
|
+
</body></html>"""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ── Pattern 3: Typographic ─────────────────────────────────────────────────────
|
|
235
|
+
def _pattern_typographic(t: dict) -> str:
|
|
236
|
+
words = t['title'].split()
|
|
237
|
+
first = words[0] if words else ""
|
|
238
|
+
rest = " ".join(words[1:]) if len(words) > 1 else ""
|
|
239
|
+
return f"""<!DOCTYPE html>
|
|
240
|
+
<html>
|
|
241
|
+
<head><meta charset="UTF-8">
|
|
242
|
+
<style>
|
|
243
|
+
{_base_css(t)}
|
|
244
|
+
html, body {{ background: {t['page_bg']}; }}
|
|
245
|
+
.page {{ background: {t['page_bg']}; }}
|
|
246
|
+
.content {{
|
|
247
|
+
position: absolute; left: 60px; top: 0; bottom: 0; right: 60px;
|
|
248
|
+
display: flex; flex-direction: column; justify-content: center;
|
|
249
|
+
}}
|
|
250
|
+
.first-word {{
|
|
251
|
+
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
|
252
|
+
font-weight: 900; font-size: 72px; line-height: 1.0;
|
|
253
|
+
color: {t['accent']}; letter-spacing: -0.02em;
|
|
254
|
+
}}
|
|
255
|
+
.rest-words {{
|
|
256
|
+
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
|
257
|
+
font-weight: 900; font-size: 72px; line-height: 1.0;
|
|
258
|
+
color: {t['dark']}; letter-spacing: -0.02em;
|
|
259
|
+
margin-bottom: 12px;
|
|
260
|
+
}}
|
|
261
|
+
.rule {{
|
|
262
|
+
width: 100%; height: 1.5px;
|
|
263
|
+
background: linear-gradient(to right, {t['accent']}, {t['accent']}40);
|
|
264
|
+
margin: 28px 0 20px;
|
|
265
|
+
}}
|
|
266
|
+
.meta-row {{
|
|
267
|
+
display: flex; justify-content: space-between; align-items: baseline;
|
|
268
|
+
}}
|
|
269
|
+
.author {{ font-size: 13px; color: {t['dark']}; letter-spacing: 0.02em; }}
|
|
270
|
+
.date {{ font-size: 12px; color: {t['muted']}; }}
|
|
271
|
+
.subtitle {{ font-size: 13px; color: {t['muted']}; margin-top: 8px; max-width: 500px; }}
|
|
272
|
+
</style>
|
|
273
|
+
</head>
|
|
274
|
+
<body>
|
|
275
|
+
<div class="page">
|
|
276
|
+
<div class="content">
|
|
277
|
+
<div class="first-word">{first}</div>
|
|
278
|
+
{'<div class="rest-words">' + rest + '</div>' if rest else ''}
|
|
279
|
+
<div class="rule"></div>
|
|
280
|
+
<div class="meta-row">
|
|
281
|
+
<div class="author">{t.get('author','')}</div>
|
|
282
|
+
<div class="date">{t.get('date','')}</div>
|
|
283
|
+
</div>
|
|
284
|
+
{'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</body></html>"""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ── Pattern 4: Dark atmospheric ────────────────────────────────────────────────
|
|
291
|
+
def _pattern_atmospheric(t: dict) -> str:
|
|
292
|
+
dot_grid = _dot_grid(
|
|
293
|
+
x0=60, y0=60, cols=16, rows=22, gap=20, r=1.5,
|
|
294
|
+
color=t["accent"], opacity=0.08
|
|
295
|
+
)
|
|
296
|
+
return f"""<!DOCTYPE html>
|
|
297
|
+
<html>
|
|
298
|
+
<head><meta charset="UTF-8">
|
|
299
|
+
<style>
|
|
300
|
+
{_base_css(t)}
|
|
301
|
+
.glow {{
|
|
302
|
+
position: absolute;
|
|
303
|
+
top: -100px; right: -80px;
|
|
304
|
+
width: 500px; height: 500px;
|
|
305
|
+
background: radial-gradient(circle, {t['accent']}2E 0%, transparent 68%);
|
|
306
|
+
border-radius: 50%;
|
|
307
|
+
}}
|
|
308
|
+
.glow2 {{
|
|
309
|
+
position: absolute;
|
|
310
|
+
bottom: -40px; left: 10%;
|
|
311
|
+
width: 300px; height: 300px;
|
|
312
|
+
background: radial-gradient(circle, {t['accent']}14 0%, transparent 70%);
|
|
313
|
+
border-radius: 50%;
|
|
314
|
+
}}
|
|
315
|
+
.content {{
|
|
316
|
+
position: absolute; left: 64px; right: 80px;
|
|
317
|
+
top: 0; bottom: 0;
|
|
318
|
+
display: flex; flex-direction: column; justify-content: center;
|
|
319
|
+
}}
|
|
320
|
+
.label {{
|
|
321
|
+
font-size: 9px; letter-spacing: 0.22em;
|
|
322
|
+
color: {t['accent']}; text-transform: uppercase; margin-bottom: 32px;
|
|
323
|
+
}}
|
|
324
|
+
.title {{
|
|
325
|
+
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
|
326
|
+
font-weight: 900; font-size: 50px; line-height: 1.05;
|
|
327
|
+
color: {t['text_light']}; max-width: 520px;
|
|
328
|
+
word-wrap: break-word; margin-bottom: 12px;
|
|
329
|
+
}}
|
|
330
|
+
.rule {{ width: 48px; height: 2px; background: {t['accent']}; margin: 24px 0 20px; }}
|
|
331
|
+
.subtitle {{
|
|
332
|
+
font-size: 13px; color: {t['muted']}; line-height: 1.6;
|
|
333
|
+
max-width: 400px; margin-bottom: 40px;
|
|
334
|
+
}}
|
|
335
|
+
.footer {{
|
|
336
|
+
position: absolute; bottom: 0; left: 0; right: 0; height: 64px;
|
|
337
|
+
border-top: 1px solid rgba(255,255,255,0.06);
|
|
338
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
339
|
+
padding: 0 64px;
|
|
340
|
+
}}
|
|
341
|
+
.footer-l {{ font-size: 10.5px; color: rgba(240,237,230,0.6); }}
|
|
342
|
+
.footer-r {{ font-size: 10.5px; color: {t['muted']}; }}
|
|
343
|
+
</style>
|
|
344
|
+
</head>
|
|
345
|
+
<body>
|
|
346
|
+
<div class="page">
|
|
347
|
+
<div class="glow"></div>
|
|
348
|
+
<div class="glow2"></div>
|
|
349
|
+
{dot_grid}
|
|
350
|
+
<div style="position:absolute;top:0;right:0;width:30%;height:3px;background:{t['accent']};"></div>
|
|
351
|
+
<div class="content">
|
|
352
|
+
<div class="label">{t.get('doc_type','').upper()} · {t.get('date','')}</div>
|
|
353
|
+
<div class="title">{t['title']}</div>
|
|
354
|
+
<div class="rule"></div>
|
|
355
|
+
{'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
|
|
356
|
+
</div>
|
|
357
|
+
<div class="footer">
|
|
358
|
+
<div class="footer-l">{t.get('author','')}</div>
|
|
359
|
+
<div class="footer-r">{t.get('date','')}</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</body></html>"""
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ── Pattern 5: Minimal — thick left bar, generous whitespace ───────────────────
|
|
366
|
+
def _pattern_minimal(t: dict) -> str:
|
|
367
|
+
"""
|
|
368
|
+
Ultra-restrained: white background, 8px left accent bar, oversized light-weight
|
|
369
|
+
title, nothing else but a hairline rule and minimal metadata. The bar is the only
|
|
370
|
+
color on the page — everything else is black on white.
|
|
371
|
+
"""
|
|
372
|
+
# Pick text color for page (minimal uses page_bg which is near-white)
|
|
373
|
+
text_dark = t.get("dark", "#111111")
|
|
374
|
+
muted = t.get("muted", "#999999")
|
|
375
|
+
accent = t["accent"]
|
|
376
|
+
|
|
377
|
+
subtitle_block = ""
|
|
378
|
+
if t.get("subtitle"):
|
|
379
|
+
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
|
380
|
+
|
|
381
|
+
return f"""<!DOCTYPE html>
|
|
382
|
+
<html>
|
|
383
|
+
<head><meta charset="UTF-8">
|
|
384
|
+
<style>
|
|
385
|
+
{_base_css(t)}
|
|
386
|
+
html, body {{ background: {t['page_bg']}; }}
|
|
387
|
+
.page {{ background: {t['page_bg']}; }}
|
|
388
|
+
|
|
389
|
+
/* Left accent bar — the only color element */
|
|
390
|
+
.bar {{
|
|
391
|
+
position: absolute;
|
|
392
|
+
top: 0; left: 0;
|
|
393
|
+
width: 8px; height: 1123px;
|
|
394
|
+
background: {accent};
|
|
395
|
+
}}
|
|
396
|
+
|
|
397
|
+
/* Main content column — offset from bar */
|
|
398
|
+
.content {{
|
|
399
|
+
position: absolute;
|
|
400
|
+
left: 64px; right: 64px;
|
|
401
|
+
top: 0; bottom: 0;
|
|
402
|
+
display: flex;
|
|
403
|
+
flex-direction: column;
|
|
404
|
+
justify-content: center;
|
|
405
|
+
padding-bottom: 40px;
|
|
406
|
+
}}
|
|
407
|
+
|
|
408
|
+
.eyebrow {{
|
|
409
|
+
font-size: 9px;
|
|
410
|
+
font-weight: 500;
|
|
411
|
+
letter-spacing: 0.28em;
|
|
412
|
+
text-transform: uppercase;
|
|
413
|
+
color: {accent};
|
|
414
|
+
margin-bottom: 36px;
|
|
415
|
+
}}
|
|
416
|
+
|
|
417
|
+
.title {{
|
|
418
|
+
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
|
419
|
+
font-weight: 300;
|
|
420
|
+
font-size: 72px;
|
|
421
|
+
line-height: 1.0;
|
|
422
|
+
color: {text_dark};
|
|
423
|
+
letter-spacing: -0.02em;
|
|
424
|
+
max-width: 580px;
|
|
425
|
+
word-wrap: break-word;
|
|
426
|
+
margin-bottom: 0;
|
|
427
|
+
}}
|
|
428
|
+
|
|
429
|
+
.rule {{
|
|
430
|
+
width: 56px;
|
|
431
|
+
height: 1px;
|
|
432
|
+
background: {text_dark};
|
|
433
|
+
margin: 36px 0 24px;
|
|
434
|
+
opacity: 0.2;
|
|
435
|
+
}}
|
|
436
|
+
|
|
437
|
+
.subtitle {{
|
|
438
|
+
font-size: 13px;
|
|
439
|
+
font-weight: 300;
|
|
440
|
+
color: {muted};
|
|
441
|
+
line-height: 1.7;
|
|
442
|
+
max-width: 460px;
|
|
443
|
+
margin-bottom: 28px;
|
|
444
|
+
}}
|
|
445
|
+
|
|
446
|
+
.meta {{
|
|
447
|
+
font-size: 10px;
|
|
448
|
+
letter-spacing: 0.06em;
|
|
449
|
+
color: {muted};
|
|
450
|
+
margin-top: 4px;
|
|
451
|
+
}}
|
|
452
|
+
</style>
|
|
453
|
+
</head>
|
|
454
|
+
<body>
|
|
455
|
+
<div class="page">
|
|
456
|
+
<div class="bar"></div>
|
|
457
|
+
<div class="content">
|
|
458
|
+
<div class="eyebrow">{t.get('doc_type','').upper()}</div>
|
|
459
|
+
<div class="title">{t['title']}</div>
|
|
460
|
+
<div class="rule"></div>
|
|
461
|
+
{subtitle_block}
|
|
462
|
+
<div class="meta">{t.get('author','')}{(' · ' + t.get('date','')) if t.get('date') else ''}</div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</body></html>"""
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ── Pattern 6: Stripe — bold horizontal bands ──────────────────────────────────
|
|
469
|
+
def _pattern_stripe(t: dict) -> str:
|
|
470
|
+
"""
|
|
471
|
+
Page divided into three bold horizontal bands:
|
|
472
|
+
- Top band (accent, ~18%): document type label
|
|
473
|
+
- Middle band (dark, ~52%): large title in white
|
|
474
|
+
- Bottom band (page bg, ~30%): author / date / subtitle
|
|
475
|
+
Hard geometry, no gradients, no textures. Newspaper / brand poster aesthetic.
|
|
476
|
+
"""
|
|
477
|
+
top_h = 200 # accent band
|
|
478
|
+
mid_h = 580 # dark band
|
|
479
|
+
bot_y = top_h + mid_h # 780
|
|
480
|
+
|
|
481
|
+
accent = t["accent"]
|
|
482
|
+
dark = t.get("cover_bg", "#1A1A2E")
|
|
483
|
+
light = t.get("page_bg", "#FAFAF8")
|
|
484
|
+
text_l = t.get("text_light", "#FFFFFF")
|
|
485
|
+
muted = t.get("muted", "#888888")
|
|
486
|
+
|
|
487
|
+
subtitle_block = ""
|
|
488
|
+
if t.get("subtitle"):
|
|
489
|
+
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
|
490
|
+
|
|
491
|
+
return f"""<!DOCTYPE html>
|
|
492
|
+
<html>
|
|
493
|
+
<head><meta charset="UTF-8">
|
|
494
|
+
<style>
|
|
495
|
+
{_base_css(t)}
|
|
496
|
+
html, body {{ background: {light}; }}
|
|
497
|
+
.page {{ background: {light}; }}
|
|
498
|
+
|
|
499
|
+
/* Three bands */
|
|
500
|
+
.band-top {{
|
|
501
|
+
position: absolute; top: 0; left: 0;
|
|
502
|
+
width: 794px; height: {top_h}px;
|
|
503
|
+
background: {accent};
|
|
504
|
+
display: flex; align-items: flex-end;
|
|
505
|
+
padding: 0 64px 24px;
|
|
506
|
+
}}
|
|
507
|
+
.band-mid {{
|
|
508
|
+
position: absolute; top: {top_h}px; left: 0;
|
|
509
|
+
width: 794px; height: {mid_h}px;
|
|
510
|
+
background: {dark};
|
|
511
|
+
display: flex; flex-direction: column; justify-content: center;
|
|
512
|
+
padding: 0 64px;
|
|
513
|
+
}}
|
|
514
|
+
.band-bot {{
|
|
515
|
+
position: absolute; top: {bot_y}px; left: 0;
|
|
516
|
+
width: 794px; height: {1123 - bot_y}px;
|
|
517
|
+
background: {light};
|
|
518
|
+
display: flex; flex-direction: column; justify-content: center;
|
|
519
|
+
padding: 0 64px;
|
|
520
|
+
}}
|
|
521
|
+
|
|
522
|
+
/* Top band — doc type in large caps */
|
|
523
|
+
.eyebrow {{
|
|
524
|
+
font-family: '{t['font_display']}', sans-serif;
|
|
525
|
+
font-size: 11px; font-weight: 700;
|
|
526
|
+
letter-spacing: 0.32em; text-transform: uppercase;
|
|
527
|
+
color: {dark}; opacity: 0.85;
|
|
528
|
+
}}
|
|
529
|
+
|
|
530
|
+
/* Mid band — title */
|
|
531
|
+
.title {{
|
|
532
|
+
font-family: '{t['font_display']}', 'Times New Roman', Georgia, serif;
|
|
533
|
+
font-weight: 900;
|
|
534
|
+
font-size: 62px;
|
|
535
|
+
line-height: 0.97;
|
|
536
|
+
color: {text_l};
|
|
537
|
+
letter-spacing: -0.02em;
|
|
538
|
+
max-width: 620px;
|
|
539
|
+
word-wrap: break-word;
|
|
540
|
+
}}
|
|
541
|
+
|
|
542
|
+
/* Thin horizontal separator between mid and bot */
|
|
543
|
+
.sep {{
|
|
544
|
+
position: absolute; top: {bot_y}px; left: 0;
|
|
545
|
+
width: 794px; height: 2px;
|
|
546
|
+
background: {accent};
|
|
547
|
+
}}
|
|
548
|
+
|
|
549
|
+
/* Bottom band */
|
|
550
|
+
.author {{
|
|
551
|
+
font-size: 13px; font-weight: 500;
|
|
552
|
+
color: {t.get('dark','#111')}; margin-bottom: 4px;
|
|
553
|
+
}}
|
|
554
|
+
.date {{ font-size: 11px; color: {muted}; margin-bottom: 12px; }}
|
|
555
|
+
.subtitle {{
|
|
556
|
+
font-size: 12px; color: {muted}; line-height: 1.6;
|
|
557
|
+
max-width: 540px;
|
|
558
|
+
}}
|
|
559
|
+
</style>
|
|
560
|
+
</head>
|
|
561
|
+
<body>
|
|
562
|
+
<div class="page">
|
|
563
|
+
<div class="band-top">
|
|
564
|
+
<div class="eyebrow">{t.get('doc_type','').upper()}</div>
|
|
565
|
+
</div>
|
|
566
|
+
<div class="band-mid">
|
|
567
|
+
<div class="title">{t['title']}</div>
|
|
568
|
+
</div>
|
|
569
|
+
<div class="sep"></div>
|
|
570
|
+
<div class="band-bot">
|
|
571
|
+
<div class="author">{t.get('author','')}</div>
|
|
572
|
+
<div class="date">{t.get('date','')}</div>
|
|
573
|
+
{subtitle_block}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</body></html>"""
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# ── Pattern 7: Diagonal — angled color split ───────────────────────────────────
|
|
580
|
+
def _pattern_diagonal(t: dict) -> str:
|
|
581
|
+
"""
|
|
582
|
+
SVG polygon cuts the page diagonally: upper-left in dark cover color,
|
|
583
|
+
lower-right in light page bg. Title sits on the dark area, metadata on light.
|
|
584
|
+
One angled edge — no gradients, no curves.
|
|
585
|
+
"""
|
|
586
|
+
dark_bg = t.get("cover_bg", "#1B2A4A")
|
|
587
|
+
light_bg = t.get("page_bg", "#FAFCFF")
|
|
588
|
+
accent = t["accent"]
|
|
589
|
+
text_l = t.get("text_light", "#F8FAFF")
|
|
590
|
+
text_d = t.get("dark", "#0F1A2E")
|
|
591
|
+
muted = t.get("muted", "#7A8A99")
|
|
592
|
+
|
|
593
|
+
# Polygon: full upper-left to ~60% down on right side
|
|
594
|
+
# Points: top-left, top-right, (794, 620), (0, 820)
|
|
595
|
+
poly = "0,0 794,0 794,620 0,820"
|
|
596
|
+
|
|
597
|
+
subtitle_block = ""
|
|
598
|
+
if t.get("subtitle"):
|
|
599
|
+
subtitle_block = f'<div class="subtitle-lt">{t["subtitle"]}</div>'
|
|
600
|
+
|
|
601
|
+
return f"""<!DOCTYPE html>
|
|
602
|
+
<html>
|
|
603
|
+
<head><meta charset="UTF-8">
|
|
604
|
+
<style>
|
|
605
|
+
{_base_css(t)}
|
|
606
|
+
html, body {{ background: {light_bg}; }}
|
|
607
|
+
.page {{ background: {light_bg}; overflow: hidden; }}
|
|
608
|
+
|
|
609
|
+
/* Title block — upper dark area */
|
|
610
|
+
.content-dark {{
|
|
611
|
+
position: absolute;
|
|
612
|
+
left: 64px; right: 64px;
|
|
613
|
+
top: 180px;
|
|
614
|
+
z-index: 2;
|
|
615
|
+
}}
|
|
616
|
+
.eyebrow {{
|
|
617
|
+
font-size: 9px; font-weight: 500;
|
|
618
|
+
letter-spacing: 0.26em; text-transform: uppercase;
|
|
619
|
+
color: {accent}; margin-bottom: 28px;
|
|
620
|
+
}}
|
|
621
|
+
.title {{
|
|
622
|
+
font-family: '{t['font_display']}', 'Helvetica Neue', sans-serif;
|
|
623
|
+
font-weight: 900;
|
|
624
|
+
font-size: 58px;
|
|
625
|
+
line-height: 1.0;
|
|
626
|
+
color: {text_l};
|
|
627
|
+
letter-spacing: -0.018em;
|
|
628
|
+
max-width: 560px;
|
|
629
|
+
word-wrap: break-word;
|
|
630
|
+
margin-bottom: 16px;
|
|
631
|
+
}}
|
|
632
|
+
.rule-accent {{
|
|
633
|
+
width: 52px; height: 3px;
|
|
634
|
+
background: {accent};
|
|
635
|
+
margin-top: 28px;
|
|
636
|
+
}}
|
|
637
|
+
|
|
638
|
+
/* Metadata — lower light area */
|
|
639
|
+
.content-light {{
|
|
640
|
+
position: absolute;
|
|
641
|
+
left: 64px; right: 64px;
|
|
642
|
+
bottom: 80px;
|
|
643
|
+
z-index: 2;
|
|
644
|
+
}}
|
|
645
|
+
.author {{
|
|
646
|
+
font-size: 12px; font-weight: 500;
|
|
647
|
+
color: {text_d}; margin-bottom: 4px;
|
|
648
|
+
}}
|
|
649
|
+
.date {{ font-size: 11px; color: {muted}; margin-bottom: 12px; }}
|
|
650
|
+
.subtitle-lt {{
|
|
651
|
+
font-size: 12px; color: {muted}; line-height: 1.6;
|
|
652
|
+
max-width: 480px;
|
|
653
|
+
}}
|
|
654
|
+
</style>
|
|
655
|
+
</head>
|
|
656
|
+
<body>
|
|
657
|
+
<div class="page">
|
|
658
|
+
<!-- Diagonal dark polygon -->
|
|
659
|
+
<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;z-index:1"
|
|
660
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
661
|
+
<polygon points="{poly}" fill="{dark_bg}"/>
|
|
662
|
+
<!-- Accent edge line along the diagonal -->
|
|
663
|
+
<line x1="0" y1="820" x2="794" y2="620"
|
|
664
|
+
stroke="{accent}" stroke-width="2.5"/>
|
|
665
|
+
</svg>
|
|
666
|
+
|
|
667
|
+
<div class="content-dark">
|
|
668
|
+
<div class="eyebrow">{t.get('doc_type','').upper()} · {t.get('date','')}</div>
|
|
669
|
+
<div class="title">{t['title']}</div>
|
|
670
|
+
<div class="rule-accent"></div>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<div class="content-light">
|
|
674
|
+
<div class="author">{t.get('author','')}</div>
|
|
675
|
+
{subtitle_block}
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
</body></html>"""
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
# ── Pattern 8: Frame — elegant inset border ────────────────────────────────────
|
|
682
|
+
def _pattern_frame(t: dict) -> str:
|
|
683
|
+
"""
|
|
684
|
+
Classic formal layout: outer thin border line inset ~28px from page edges,
|
|
685
|
+
inner accent strip at top and bottom inside the frame.
|
|
686
|
+
Title centered in the frame space, classical serif typography.
|
|
687
|
+
Used for: academic papers, formal reports, legal docs, annual reports.
|
|
688
|
+
"""
|
|
689
|
+
bg = t.get("cover_bg", "#FAF8F3")
|
|
690
|
+
accent = t["accent"]
|
|
691
|
+
dark = t.get("dark", "#2A1A0A")
|
|
692
|
+
muted = t.get("muted", "#9A8A78")
|
|
693
|
+
|
|
694
|
+
pad = 28 # frame inset from page edge
|
|
695
|
+
inner_w = 794 - 2 * pad
|
|
696
|
+
inner_h = 1123 - 2 * pad
|
|
697
|
+
|
|
698
|
+
subtitle_block = ""
|
|
699
|
+
if t.get("subtitle"):
|
|
700
|
+
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
|
701
|
+
|
|
702
|
+
return f"""<!DOCTYPE html>
|
|
703
|
+
<html>
|
|
704
|
+
<head><meta charset="UTF-8">
|
|
705
|
+
<style>
|
|
706
|
+
{_base_css(t)}
|
|
707
|
+
html, body {{ background: {bg}; }}
|
|
708
|
+
.page {{ background: {bg}; }}
|
|
709
|
+
|
|
710
|
+
/* Outer frame rectangle */
|
|
711
|
+
.frame {{
|
|
712
|
+
position: absolute;
|
|
713
|
+
top: {pad}px; left: {pad}px;
|
|
714
|
+
width: {inner_w}px; height: {inner_h}px;
|
|
715
|
+
border: 1.2px solid {dark};
|
|
716
|
+
opacity: 0.35;
|
|
717
|
+
}}
|
|
718
|
+
|
|
719
|
+
/* Accent strips inside top and bottom of frame */
|
|
720
|
+
.frame-top-accent {{
|
|
721
|
+
position: absolute;
|
|
722
|
+
top: {pad + 10}px; left: {pad + 10}px;
|
|
723
|
+
width: {inner_w - 20}px; height: 3px;
|
|
724
|
+
background: {accent};
|
|
725
|
+
}}
|
|
726
|
+
.frame-bot-accent {{
|
|
727
|
+
position: absolute;
|
|
728
|
+
bottom: {pad + 10}px; left: {pad + 10}px;
|
|
729
|
+
width: {inner_w - 20}px; height: 3px;
|
|
730
|
+
background: {accent};
|
|
731
|
+
}}
|
|
732
|
+
|
|
733
|
+
/* Corner ornament squares */
|
|
734
|
+
.corner {{
|
|
735
|
+
position: absolute;
|
|
736
|
+
width: 8px; height: 8px;
|
|
737
|
+
background: {accent};
|
|
738
|
+
opacity: 0.6;
|
|
739
|
+
}}
|
|
740
|
+
.tl {{ top: {pad - 4}px; left: {pad - 4}px; }}
|
|
741
|
+
.tr {{ top: {pad - 4}px; right: {pad - 4}px; }}
|
|
742
|
+
.bl {{ bottom: {pad - 4}px; left: {pad - 4}px; }}
|
|
743
|
+
.br {{ bottom: {pad - 4}px; right: {pad - 4}px; }}
|
|
744
|
+
|
|
745
|
+
/* Main content centered in frame */
|
|
746
|
+
.content {{
|
|
747
|
+
position: absolute;
|
|
748
|
+
left: {pad + 56}px; right: {pad + 56}px;
|
|
749
|
+
top: 0; bottom: 0;
|
|
750
|
+
display: flex;
|
|
751
|
+
flex-direction: column;
|
|
752
|
+
align-items: center;
|
|
753
|
+
justify-content: center;
|
|
754
|
+
text-align: center;
|
|
755
|
+
}}
|
|
756
|
+
|
|
757
|
+
.eyebrow {{
|
|
758
|
+
font-size: 8.5px;
|
|
759
|
+
font-weight: 500;
|
|
760
|
+
letter-spacing: 0.30em;
|
|
761
|
+
text-transform: uppercase;
|
|
762
|
+
color: {accent};
|
|
763
|
+
margin-bottom: 44px;
|
|
764
|
+
}}
|
|
765
|
+
|
|
766
|
+
.rule-top {{
|
|
767
|
+
width: 60px; height: 1px;
|
|
768
|
+
background: {dark};
|
|
769
|
+
opacity: 0.3;
|
|
770
|
+
margin-bottom: 28px;
|
|
771
|
+
}}
|
|
772
|
+
|
|
773
|
+
.title {{
|
|
774
|
+
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
|
775
|
+
font-weight: 400;
|
|
776
|
+
font-size: 44px;
|
|
777
|
+
line-height: 1.25;
|
|
778
|
+
color: {dark};
|
|
779
|
+
letter-spacing: 0.01em;
|
|
780
|
+
max-width: 540px;
|
|
781
|
+
word-wrap: break-word;
|
|
782
|
+
margin-bottom: 0;
|
|
783
|
+
}}
|
|
784
|
+
|
|
785
|
+
.rule-mid {{
|
|
786
|
+
width: 40px; height: 1.5px;
|
|
787
|
+
background: {accent};
|
|
788
|
+
margin: 28px 0 20px;
|
|
789
|
+
}}
|
|
790
|
+
|
|
791
|
+
.subtitle {{
|
|
792
|
+
font-size: 13px;
|
|
793
|
+
font-weight: 300;
|
|
794
|
+
font-style: italic;
|
|
795
|
+
color: {muted};
|
|
796
|
+
line-height: 1.6;
|
|
797
|
+
max-width: 400px;
|
|
798
|
+
margin-bottom: 20px;
|
|
799
|
+
}}
|
|
800
|
+
|
|
801
|
+
.meta {{
|
|
802
|
+
font-size: 10px;
|
|
803
|
+
letter-spacing: 0.08em;
|
|
804
|
+
color: {muted};
|
|
805
|
+
margin-top: 8px;
|
|
806
|
+
}}
|
|
807
|
+
</style>
|
|
808
|
+
</head>
|
|
809
|
+
<body>
|
|
810
|
+
<div class="page">
|
|
811
|
+
<div class="frame"></div>
|
|
812
|
+
<div class="frame-top-accent"></div>
|
|
813
|
+
<div class="frame-bot-accent"></div>
|
|
814
|
+
<div class="corner tl"></div>
|
|
815
|
+
<div class="corner tr"></div>
|
|
816
|
+
<div class="corner bl"></div>
|
|
817
|
+
<div class="corner br"></div>
|
|
818
|
+
|
|
819
|
+
<div class="content">
|
|
820
|
+
<div class="eyebrow">{t.get('doc_type','').upper()}</div>
|
|
821
|
+
<div class="rule-top"></div>
|
|
822
|
+
<div class="title">{t['title']}</div>
|
|
823
|
+
<div class="rule-mid"></div>
|
|
824
|
+
{subtitle_block}
|
|
825
|
+
<div class="meta">{t.get('author','')}{(' · ' + t.get('date','')) if t.get('date') else ''}</div>
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
</body></html>"""
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
# ── Pattern 9: Editorial — oversized ghost letter + bold type ──────────────────
|
|
832
|
+
def _pattern_editorial(t: dict) -> str:
|
|
833
|
+
"""
|
|
834
|
+
Magazine / editorial feel:
|
|
835
|
+
- Oversized first-letter of title as a ghost background element (8–12% opacity)
|
|
836
|
+
- Bold category label at top in accent
|
|
837
|
+
- Title in very large condensed weight, flush-left
|
|
838
|
+
- Thin full-width rule separating title from metadata
|
|
839
|
+
- Author / date bottom-left, page type bottom-right
|
|
840
|
+
Designed for editorial reports, annual reviews, magazine-format content.
|
|
841
|
+
"""
|
|
842
|
+
bg = t.get("cover_bg", "#FFFFFF")
|
|
843
|
+
accent = t["accent"]
|
|
844
|
+
dark = t.get("dark", "#0A0A0A")
|
|
845
|
+
muted = t.get("muted", "#777777")
|
|
846
|
+
text_l = t.get("text_light", "#FFFFFF")
|
|
847
|
+
|
|
848
|
+
# Ghost letter — first character of title
|
|
849
|
+
ghost = t['title'][0].upper() if t['title'] else "A"
|
|
850
|
+
|
|
851
|
+
subtitle_block = ""
|
|
852
|
+
if t.get("subtitle"):
|
|
853
|
+
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
|
854
|
+
|
|
855
|
+
# Determine if background is dark (use light text) or light (use dark text)
|
|
856
|
+
is_dark_bg = (
|
|
857
|
+
bg.startswith("#0") or bg.startswith("#1") or bg.startswith("#2")
|
|
858
|
+
)
|
|
859
|
+
title_color = text_l if is_dark_bg else dark # noqa: F841
|
|
860
|
+
body_color = text_l if is_dark_bg else dark
|
|
861
|
+
|
|
862
|
+
return f"""<!DOCTYPE html>
|
|
863
|
+
<html>
|
|
864
|
+
<head><meta charset="UTF-8">
|
|
865
|
+
<style>
|
|
866
|
+
{_base_css(t)}
|
|
867
|
+
html, body {{ background: {bg}; }}
|
|
868
|
+
.page {{ background: {bg}; }}
|
|
869
|
+
|
|
870
|
+
/* Ghost letter — background texture */
|
|
871
|
+
.ghost {{
|
|
872
|
+
position: absolute;
|
|
873
|
+
right: -60px; top: -40px;
|
|
874
|
+
font-family: '{t['font_display']}', 'Arial Black', sans-serif;
|
|
875
|
+
font-weight: 900;
|
|
876
|
+
font-size: 680px;
|
|
877
|
+
line-height: 1;
|
|
878
|
+
color: {dark};
|
|
879
|
+
opacity: 0.055;
|
|
880
|
+
user-select: none;
|
|
881
|
+
letter-spacing: -0.05em;
|
|
882
|
+
}}
|
|
883
|
+
|
|
884
|
+
/* Top bar: accent stripe */
|
|
885
|
+
.topbar {{
|
|
886
|
+
position: absolute;
|
|
887
|
+
top: 0; left: 0; right: 0;
|
|
888
|
+
height: 5px;
|
|
889
|
+
background: {accent};
|
|
890
|
+
}}
|
|
891
|
+
|
|
892
|
+
/* Category label */
|
|
893
|
+
.category {{
|
|
894
|
+
position: absolute;
|
|
895
|
+
top: 40px; left: 60px;
|
|
896
|
+
font-size: 9px; font-weight: 700;
|
|
897
|
+
letter-spacing: 0.30em; text-transform: uppercase;
|
|
898
|
+
color: {accent};
|
|
899
|
+
}}
|
|
900
|
+
|
|
901
|
+
/* Main title block */
|
|
902
|
+
.content {{
|
|
903
|
+
position: absolute;
|
|
904
|
+
left: 60px; right: 60px;
|
|
905
|
+
top: 0; bottom: 0;
|
|
906
|
+
display: flex;
|
|
907
|
+
flex-direction: column;
|
|
908
|
+
justify-content: center;
|
|
909
|
+
padding-bottom: 80px;
|
|
910
|
+
}}
|
|
911
|
+
|
|
912
|
+
.title {{
|
|
913
|
+
font-family: '{t['font_display']}', 'Arial Black', Impact, sans-serif;
|
|
914
|
+
font-weight: 900;
|
|
915
|
+
font-size: 80px;
|
|
916
|
+
line-height: 0.92;
|
|
917
|
+
color: {body_color};
|
|
918
|
+
letter-spacing: -0.03em;
|
|
919
|
+
max-width: 620px;
|
|
920
|
+
word-wrap: break-word;
|
|
921
|
+
text-transform: uppercase;
|
|
922
|
+
}}
|
|
923
|
+
|
|
924
|
+
.subtitle {{
|
|
925
|
+
font-size: 14px;
|
|
926
|
+
font-weight: 400;
|
|
927
|
+
color: {muted};
|
|
928
|
+
line-height: 1.6;
|
|
929
|
+
max-width: 500px;
|
|
930
|
+
margin-top: 20px;
|
|
931
|
+
}}
|
|
932
|
+
|
|
933
|
+
/* Full-width rule above footer */
|
|
934
|
+
.footer-rule {{
|
|
935
|
+
position: absolute;
|
|
936
|
+
bottom: 80px; left: 60px; right: 60px;
|
|
937
|
+
height: 1px;
|
|
938
|
+
background: {body_color};
|
|
939
|
+
opacity: 0.15;
|
|
940
|
+
}}
|
|
941
|
+
|
|
942
|
+
/* Footer row */
|
|
943
|
+
.footer {{
|
|
944
|
+
position: absolute;
|
|
945
|
+
bottom: 44px; left: 60px; right: 60px;
|
|
946
|
+
display: flex;
|
|
947
|
+
justify-content: space-between;
|
|
948
|
+
align-items: baseline;
|
|
949
|
+
}}
|
|
950
|
+
.footer-author {{ font-size: 11px; color: {muted}; letter-spacing: 0.04em; }}
|
|
951
|
+
.footer-date {{ font-size: 10px; color: {muted}; letter-spacing: 0.04em; }}
|
|
952
|
+
</style>
|
|
953
|
+
</head>
|
|
954
|
+
<body>
|
|
955
|
+
<div class="page">
|
|
956
|
+
<div class="ghost">{ghost}</div>
|
|
957
|
+
<div class="topbar"></div>
|
|
958
|
+
<div class="category">{t.get('doc_type','').upper()}</div>
|
|
959
|
+
|
|
960
|
+
<div class="content">
|
|
961
|
+
<div class="title">{t['title']}</div>
|
|
962
|
+
{subtitle_block}
|
|
963
|
+
</div>
|
|
964
|
+
|
|
965
|
+
<div class="footer-rule"></div>
|
|
966
|
+
<div class="footer">
|
|
967
|
+
<div class="footer-author">{t.get('author','')}</div>
|
|
968
|
+
<div class="footer-date">{t.get('date','')}</div>
|
|
969
|
+
</div>
|
|
970
|
+
</div>
|
|
971
|
+
</body></html>"""
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
# ── Pattern 10: Magazine — elegant centered with optional hero image ────────────
|
|
975
|
+
def _pattern_magazine(t: dict) -> str:
|
|
976
|
+
"""
|
|
977
|
+
Upscale centered layout: company name + accent rule at top, large serif title,
|
|
978
|
+
decorative rule, italic subtitle, optional hero image, abstract block, author.
|
|
979
|
+
Used for: annual reports, strategic documents, formal publications.
|
|
980
|
+
"""
|
|
981
|
+
bg = t.get("cover_bg", "#F2F0EC")
|
|
982
|
+
accent = t["accent"]
|
|
983
|
+
dark = t.get("dark", "#0D1A2B")
|
|
984
|
+
muted = t.get("muted", "#888888")
|
|
985
|
+
org = t.get("doc_type", "").upper()
|
|
986
|
+
img_url = t.get("cover_image", "")
|
|
987
|
+
|
|
988
|
+
subtitle_block = ""
|
|
989
|
+
if t.get("subtitle"):
|
|
990
|
+
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
|
991
|
+
|
|
992
|
+
image_block = ""
|
|
993
|
+
if img_url:
|
|
994
|
+
image_block = f"""
|
|
995
|
+
<div style="text-align:center;margin:32px 0 28px;">
|
|
996
|
+
<img src="{img_url}" style="max-width:340px;max-height:220px;
|
|
997
|
+
object-fit:cover;display:inline-block;"/>
|
|
998
|
+
</div>"""
|
|
999
|
+
|
|
1000
|
+
abstract_block = ""
|
|
1001
|
+
if t.get("abstract"):
|
|
1002
|
+
abstract_block = f"""
|
|
1003
|
+
<div style="font-size:11px;line-height:1.7;color:{muted};
|
|
1004
|
+
text-align:justify;max-width:560px;margin:0 auto 0;">
|
|
1005
|
+
<span style="font-weight:700;color:{accent};">Abstract:</span>
|
|
1006
|
+
{t['abstract']}
|
|
1007
|
+
</div>"""
|
|
1008
|
+
|
|
1009
|
+
return f"""<!DOCTYPE html>
|
|
1010
|
+
<html>
|
|
1011
|
+
<head><meta charset="UTF-8">
|
|
1012
|
+
<style>
|
|
1013
|
+
{_base_css(t)}
|
|
1014
|
+
html, body {{ background: {bg}; }}
|
|
1015
|
+
.page {{ background: {bg}; display:flex; flex-direction:column;
|
|
1016
|
+
align-items:center; justify-content:center; padding:60px 80px; }}
|
|
1017
|
+
|
|
1018
|
+
.org-name {{
|
|
1019
|
+
font-size: 9px; font-weight: 500; letter-spacing: 0.30em;
|
|
1020
|
+
text-transform: uppercase; color: {dark}; text-align:center;
|
|
1021
|
+
margin-bottom: 10px;
|
|
1022
|
+
}}
|
|
1023
|
+
.org-rule {{
|
|
1024
|
+
width: 56px; height: 2px; background: {accent};
|
|
1025
|
+
margin: 0 auto 52px;
|
|
1026
|
+
}}
|
|
1027
|
+
.title {{
|
|
1028
|
+
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
|
1029
|
+
font-weight: 700; font-size: 52px; line-height: 1.08;
|
|
1030
|
+
color: {dark}; text-align: center; letter-spacing: -0.015em;
|
|
1031
|
+
max-width: 560px; word-wrap: break-word; margin-bottom: 18px;
|
|
1032
|
+
}}
|
|
1033
|
+
.title-rule {{
|
|
1034
|
+
width: 44px; height: 2.5px; background: {accent};
|
|
1035
|
+
margin: 0 auto 20px;
|
|
1036
|
+
}}
|
|
1037
|
+
.subtitle {{
|
|
1038
|
+
font-family: '{t['font_display']}', Georgia, serif;
|
|
1039
|
+
font-style: italic; font-size: 14px; color: {muted};
|
|
1040
|
+
text-align: center; line-height: 1.5; max-width: 440px;
|
|
1041
|
+
margin: 0 auto;
|
|
1042
|
+
}}
|
|
1043
|
+
.separator {{
|
|
1044
|
+
width: 100%; max-width: 620px; height: 1px;
|
|
1045
|
+
background: {dark}; opacity: 0.12;
|
|
1046
|
+
margin: 28px auto;
|
|
1047
|
+
}}
|
|
1048
|
+
.author-name {{
|
|
1049
|
+
font-family: '{t['font_display']}', Georgia, serif;
|
|
1050
|
+
font-size: 16px; font-weight: 700; color: {accent};
|
|
1051
|
+
text-align: center; margin-bottom: 6px;
|
|
1052
|
+
}}
|
|
1053
|
+
.date-line {{
|
|
1054
|
+
font-size: 11px; color: {muted}; text-align: center;
|
|
1055
|
+
letter-spacing: 0.03em;
|
|
1056
|
+
}}
|
|
1057
|
+
</style>
|
|
1058
|
+
</head>
|
|
1059
|
+
<body>
|
|
1060
|
+
<div class="page">
|
|
1061
|
+
<div class="org-name">{org}</div>
|
|
1062
|
+
<div class="org-rule"></div>
|
|
1063
|
+
<div class="title">{t['title']}</div>
|
|
1064
|
+
<div class="title-rule"></div>
|
|
1065
|
+
{subtitle_block}
|
|
1066
|
+
{image_block}
|
|
1067
|
+
{abstract_block}
|
|
1068
|
+
{'<div class="separator"></div>' if (t.get('abstract') or img_url) else '<div style="margin:28px 0;"></div>'}
|
|
1069
|
+
<div class="author-name">{t.get('author','')}</div>
|
|
1070
|
+
<div class="date-line">{t.get('date','')}</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
</body></html>"""
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
# ── Pattern 11: Darkroom — dark magazine variant ────────────────────────────────
|
|
1076
|
+
def _pattern_darkroom(t: dict) -> str:
|
|
1077
|
+
"""
|
|
1078
|
+
Dark-background centered layout. Same structure as magazine but inverted:
|
|
1079
|
+
deep navy page, white/silver text, accent rules in lighter tone.
|
|
1080
|
+
Used for: premium reports, tech annual reviews, dark-themed documents.
|
|
1081
|
+
"""
|
|
1082
|
+
bg = t.get("cover_bg", "#151C27")
|
|
1083
|
+
accent = t["accent"]
|
|
1084
|
+
text_l = t.get("text_light", "#F0EDE6")
|
|
1085
|
+
muted = t.get("muted", "#8A9AB0")
|
|
1086
|
+
org = t.get("doc_type", "").upper()
|
|
1087
|
+
img_url = t.get("cover_image", "")
|
|
1088
|
+
|
|
1089
|
+
subtitle_block = ""
|
|
1090
|
+
if t.get("subtitle"):
|
|
1091
|
+
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
|
1092
|
+
|
|
1093
|
+
image_block = ""
|
|
1094
|
+
if img_url:
|
|
1095
|
+
image_block = f"""
|
|
1096
|
+
<div style="text-align:center;margin:32px 0 28px;">
|
|
1097
|
+
<img src="{img_url}" style="max-width:340px;max-height:220px;
|
|
1098
|
+
object-fit:cover;display:inline-block;
|
|
1099
|
+
filter:grayscale(20%) brightness(0.9);"/>
|
|
1100
|
+
</div>"""
|
|
1101
|
+
|
|
1102
|
+
abstract_block = ""
|
|
1103
|
+
if t.get("abstract"):
|
|
1104
|
+
abstract_block = f"""
|
|
1105
|
+
<div style="font-size:11px;line-height:1.7;color:{muted};
|
|
1106
|
+
text-align:justify;max-width:560px;margin:0 auto 0;">
|
|
1107
|
+
<span style="font-weight:700;color:{accent};">Abstract:</span>
|
|
1108
|
+
{t['abstract']}
|
|
1109
|
+
</div>"""
|
|
1110
|
+
|
|
1111
|
+
return f"""<!DOCTYPE html>
|
|
1112
|
+
<html>
|
|
1113
|
+
<head><meta charset="UTF-8">
|
|
1114
|
+
<style>
|
|
1115
|
+
{_base_css(t)}
|
|
1116
|
+
html, body {{ background: {bg}; }}
|
|
1117
|
+
.page {{ background: {bg}; display:flex; flex-direction:column;
|
|
1118
|
+
align-items:center; justify-content:center; padding:60px 80px; }}
|
|
1119
|
+
|
|
1120
|
+
.org-name {{
|
|
1121
|
+
font-size: 9px; font-weight: 500; letter-spacing: 0.30em;
|
|
1122
|
+
text-transform: uppercase; color: {text_l}; text-align:center;
|
|
1123
|
+
opacity: 0.75; margin-bottom: 10px;
|
|
1124
|
+
}}
|
|
1125
|
+
.org-rule {{
|
|
1126
|
+
width: 56px; height: 2px; background: {text_l};
|
|
1127
|
+
opacity: 0.35; margin: 0 auto 52px;
|
|
1128
|
+
}}
|
|
1129
|
+
.title {{
|
|
1130
|
+
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
|
1131
|
+
font-weight: 700; font-size: 52px; line-height: 1.08;
|
|
1132
|
+
color: {text_l}; text-align: center; letter-spacing: -0.015em;
|
|
1133
|
+
max-width: 560px; word-wrap: break-word; margin-bottom: 18px;
|
|
1134
|
+
}}
|
|
1135
|
+
.title-rule {{
|
|
1136
|
+
width: 44px; height: 2.5px; background: {text_l};
|
|
1137
|
+
opacity: 0.35; margin: 0 auto 20px;
|
|
1138
|
+
}}
|
|
1139
|
+
.subtitle {{
|
|
1140
|
+
font-family: '{t['font_display']}', Georgia, serif;
|
|
1141
|
+
font-style: italic; font-size: 14px; color: {muted};
|
|
1142
|
+
text-align: center; line-height: 1.5; max-width: 440px;
|
|
1143
|
+
margin: 0 auto;
|
|
1144
|
+
}}
|
|
1145
|
+
.separator {{
|
|
1146
|
+
width: 100%; max-width: 620px; height: 1px;
|
|
1147
|
+
background: {text_l}; opacity: 0.12;
|
|
1148
|
+
margin: 28px auto;
|
|
1149
|
+
}}
|
|
1150
|
+
.author-name {{
|
|
1151
|
+
font-family: '{t['font_display']}', Georgia, serif;
|
|
1152
|
+
font-size: 16px; font-weight: 700; color: {text_l};
|
|
1153
|
+
text-align: center; margin-bottom: 6px;
|
|
1154
|
+
}}
|
|
1155
|
+
.date-line {{
|
|
1156
|
+
font-size: 11px; color: {muted}; text-align: center;
|
|
1157
|
+
letter-spacing: 0.03em;
|
|
1158
|
+
}}
|
|
1159
|
+
</style>
|
|
1160
|
+
</head>
|
|
1161
|
+
<body>
|
|
1162
|
+
<div class="page">
|
|
1163
|
+
<div class="org-name">{org}</div>
|
|
1164
|
+
<div class="org-rule"></div>
|
|
1165
|
+
<div class="title">{t['title']}</div>
|
|
1166
|
+
<div class="title-rule"></div>
|
|
1167
|
+
{subtitle_block}
|
|
1168
|
+
{image_block}
|
|
1169
|
+
{abstract_block}
|
|
1170
|
+
{'<div class="separator"></div>' if (t.get('abstract') or img_url) else '<div style="margin:28px 0;"></div>'}
|
|
1171
|
+
<div class="author-name">{t.get('author','')}</div>
|
|
1172
|
+
<div class="date-line">{t.get('date','')}</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
</body></html>"""
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
# ── Pattern 12: Terminal — cyber/hacker aesthetic ───────────────────────────────
|
|
1178
|
+
def _pattern_terminal(t: dict) -> str:
|
|
1179
|
+
"""
|
|
1180
|
+
Dark terminal/IDE aesthetic: grid overlay, monospace font, neon accent,
|
|
1181
|
+
corner brackets around the title block, status bar at bottom.
|
|
1182
|
+
Used for: tech reports, developer docs, security audits, system documentation.
|
|
1183
|
+
"""
|
|
1184
|
+
bg = t.get("cover_bg", "#0D1117")
|
|
1185
|
+
accent = t["accent"]
|
|
1186
|
+
text_l = t.get("text_light", "#E6EDF3")
|
|
1187
|
+
muted = t.get("muted", "#48897C")
|
|
1188
|
+
dark = t.get("dark", "#010409")
|
|
1189
|
+
org = t.get("doc_type", "DOCUMENT").upper()
|
|
1190
|
+
date_s = t.get("date", "")
|
|
1191
|
+
author = t.get("author", "")
|
|
1192
|
+
|
|
1193
|
+
subtitle_line = ""
|
|
1194
|
+
if t.get("subtitle"):
|
|
1195
|
+
subtitle_line = f'<div class="subtitle">> {t["subtitle"]}</div>'
|
|
1196
|
+
|
|
1197
|
+
abstract_block = ""
|
|
1198
|
+
if t.get("abstract"):
|
|
1199
|
+
abstract_block = f"""
|
|
1200
|
+
<div class="abstract-text">{t['abstract']}</div>"""
|
|
1201
|
+
|
|
1202
|
+
# grid overlay: horizontal + vertical lines
|
|
1203
|
+
h_lines = "".join(
|
|
1204
|
+
f'<line x1="0" y1="{y}" x2="794" y2="{y}" stroke="{accent}" stroke-width="0.4"/>'
|
|
1205
|
+
for y in range(0, 1124, 48)
|
|
1206
|
+
)
|
|
1207
|
+
v_lines = "".join(
|
|
1208
|
+
f'<line x1="{x}" y1="0" x2="{x}" y2="1123" stroke="{accent}" stroke-width="0.4"/>'
|
|
1209
|
+
for x in range(0, 795, 48)
|
|
1210
|
+
)
|
|
1211
|
+
grid_svg = (
|
|
1212
|
+
f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
|
|
1213
|
+
f'pointer-events:none;opacity:0.07" xmlns="http://www.w3.org/2000/svg">'
|
|
1214
|
+
+ h_lines + v_lines + "</svg>"
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
return f"""<!DOCTYPE html>
|
|
1218
|
+
<html>
|
|
1219
|
+
<head><meta charset="UTF-8">
|
|
1220
|
+
<style>
|
|
1221
|
+
{_base_css(t)}
|
|
1222
|
+
html, body {{ background: {bg}; }}
|
|
1223
|
+
.page {{ background: {bg}; }}
|
|
1224
|
+
|
|
1225
|
+
/* Terminal label — top */
|
|
1226
|
+
.term-label {{
|
|
1227
|
+
position: absolute; top: 44px; left: 56px; right: 56px;
|
|
1228
|
+
display: flex; align-items: center; gap: 10px;
|
|
1229
|
+
}}
|
|
1230
|
+
.dot {{
|
|
1231
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
1232
|
+
background: {accent}; flex-shrink: 0;
|
|
1233
|
+
}}
|
|
1234
|
+
.term-meta {{
|
|
1235
|
+
font-family: '{t['font_body']}', 'Courier New', monospace;
|
|
1236
|
+
font-size: 10px; color: {accent}; letter-spacing: 0.08em;
|
|
1237
|
+
text-transform: uppercase;
|
|
1238
|
+
}}
|
|
1239
|
+
|
|
1240
|
+
/* Title bracket block */
|
|
1241
|
+
.bracket-block {{
|
|
1242
|
+
position: absolute;
|
|
1243
|
+
top: 310px; left: 56px; right: 56px;
|
|
1244
|
+
border-left: 2px solid {accent}; border-top: 2px solid {accent};
|
|
1245
|
+
padding: 24px 28px 28px;
|
|
1246
|
+
box-shadow: inset 0 0 0 0;
|
|
1247
|
+
}}
|
|
1248
|
+
.bracket-block::after {{
|
|
1249
|
+
content: '';
|
|
1250
|
+
position: absolute;
|
|
1251
|
+
bottom: 0; right: 0;
|
|
1252
|
+
width: 32px; height: 2px;
|
|
1253
|
+
background: {accent};
|
|
1254
|
+
}}
|
|
1255
|
+
.bracket-block::before {{
|
|
1256
|
+
content: '';
|
|
1257
|
+
position: absolute;
|
|
1258
|
+
bottom: 0; right: 0;
|
|
1259
|
+
width: 2px; height: 32px;
|
|
1260
|
+
background: {accent};
|
|
1261
|
+
}}
|
|
1262
|
+
|
|
1263
|
+
.title {{
|
|
1264
|
+
font-family: '{t['font_display']}', 'Courier New', monospace;
|
|
1265
|
+
font-weight: 700; font-size: 46px; line-height: 1.05;
|
|
1266
|
+
color: {text_l}; letter-spacing: 0.01em;
|
|
1267
|
+
text-transform: uppercase;
|
|
1268
|
+
word-wrap: break-word; margin-bottom: 16px;
|
|
1269
|
+
}}
|
|
1270
|
+
.subtitle {{
|
|
1271
|
+
font-family: '{t['font_body']}', 'Courier New', monospace;
|
|
1272
|
+
font-size: 13px; color: {accent};
|
|
1273
|
+
line-height: 1.5; letter-spacing: 0.02em;
|
|
1274
|
+
margin-top: 8px;
|
|
1275
|
+
}}
|
|
1276
|
+
|
|
1277
|
+
/* Content block below brackets */
|
|
1278
|
+
.content-lower {{
|
|
1279
|
+
position: absolute;
|
|
1280
|
+
top: 640px; left: 56px; right: 56px;
|
|
1281
|
+
display: flex; gap: 40px; align-items: flex-start;
|
|
1282
|
+
}}
|
|
1283
|
+
.abstract-text {{
|
|
1284
|
+
font-family: '{t['font_body']}', 'Courier New', monospace;
|
|
1285
|
+
font-size: 10.5px; line-height: 1.8; color: {muted};
|
|
1286
|
+
flex: 1;
|
|
1287
|
+
}}
|
|
1288
|
+
.author-block {{
|
|
1289
|
+
text-align: right; flex-shrink: 0; min-width: 160px;
|
|
1290
|
+
}}
|
|
1291
|
+
.author-label {{
|
|
1292
|
+
font-family: '{t['font_body']}', monospace;
|
|
1293
|
+
font-size: 8px; letter-spacing: 0.20em; color: {muted};
|
|
1294
|
+
text-transform: uppercase; margin-bottom: 6px;
|
|
1295
|
+
}}
|
|
1296
|
+
.author-name {{
|
|
1297
|
+
font-family: '{t['font_body']}', monospace;
|
|
1298
|
+
font-size: 14px; font-weight: 700; color: {text_l};
|
|
1299
|
+
}}
|
|
1300
|
+
.author-org {{
|
|
1301
|
+
font-family: '{t['font_body']}', monospace;
|
|
1302
|
+
font-size: 10px; color: {accent}; margin-top: 4px;
|
|
1303
|
+
}}
|
|
1304
|
+
|
|
1305
|
+
/* Bottom status bar */
|
|
1306
|
+
.statusbar {{
|
|
1307
|
+
position: absolute; bottom: 0; left: 0; right: 0;
|
|
1308
|
+
height: 36px; background: {accent}; opacity: 0.12;
|
|
1309
|
+
}}
|
|
1310
|
+
.statusbar-text {{
|
|
1311
|
+
position: absolute; bottom: 0; left: 0; right: 0;
|
|
1312
|
+
height: 36px; display: flex; align-items: center;
|
|
1313
|
+
justify-content: space-between; padding: 0 56px;
|
|
1314
|
+
}}
|
|
1315
|
+
.sb-item {{
|
|
1316
|
+
font-family: '{t['font_body']}', monospace;
|
|
1317
|
+
font-size: 9px; color: {muted}; letter-spacing: 0.12em;
|
|
1318
|
+
text-transform: uppercase;
|
|
1319
|
+
}}
|
|
1320
|
+
</style>
|
|
1321
|
+
</head>
|
|
1322
|
+
<body>
|
|
1323
|
+
<div class="page">
|
|
1324
|
+
{grid_svg}
|
|
1325
|
+
|
|
1326
|
+
<div class="term-label">
|
|
1327
|
+
<div class="dot"></div>
|
|
1328
|
+
<div class="term-meta">SYSTEM_REPORT // {date_s}</div>
|
|
1329
|
+
</div>
|
|
1330
|
+
|
|
1331
|
+
<div class="bracket-block">
|
|
1332
|
+
<div class="title">{t['title']}</div>
|
|
1333
|
+
{subtitle_line}
|
|
1334
|
+
</div>
|
|
1335
|
+
|
|
1336
|
+
<div class="content-lower">
|
|
1337
|
+
{abstract_block}
|
|
1338
|
+
<div class="author-block">
|
|
1339
|
+
<div class="author-label">AUTHOR_ID</div>
|
|
1340
|
+
<div class="author-name">{author}</div>
|
|
1341
|
+
<div class="author-org">{org}</div>
|
|
1342
|
+
</div>
|
|
1343
|
+
</div>
|
|
1344
|
+
|
|
1345
|
+
<div class="statusbar"></div>
|
|
1346
|
+
<div class="statusbar-text">
|
|
1347
|
+
<div class="sb-item">Ln 1, Col 1</div>
|
|
1348
|
+
<div class="sb-item">UTF-8</div>
|
|
1349
|
+
<div class="sb-item">GENERATED_BY_COVERGENIUS</div>
|
|
1350
|
+
</div>
|
|
1351
|
+
</div>
|
|
1352
|
+
</body></html>"""
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
# ── Pattern 13: Poster — bold sidebar + oversized type ─────────────────────────
|
|
1356
|
+
def _pattern_poster(t: dict) -> str:
|
|
1357
|
+
"""
|
|
1358
|
+
Bold minimalist poster: thick vertical sidebar on the left, oversized all-caps
|
|
1359
|
+
title, typewriter-style metadata. Optional thumbnail on the right side.
|
|
1360
|
+
Used for: portfolios, creative reports, journalism, photography books.
|
|
1361
|
+
"""
|
|
1362
|
+
bg = t.get("cover_bg", "#FFFFFF")
|
|
1363
|
+
accent = t["accent"] # typically black or strong dark
|
|
1364
|
+
dark = t.get("dark", "#0A0A0A")
|
|
1365
|
+
muted = t.get("muted", "#888888")
|
|
1366
|
+
text_l = t.get("text_light", "#FFFFFF")
|
|
1367
|
+
img_url = t.get("cover_image", "")
|
|
1368
|
+
|
|
1369
|
+
sidebar_w = 52
|
|
1370
|
+
|
|
1371
|
+
subtitle_block = ""
|
|
1372
|
+
if t.get("subtitle"):
|
|
1373
|
+
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
|
1374
|
+
|
|
1375
|
+
image_block = ""
|
|
1376
|
+
if img_url:
|
|
1377
|
+
image_block = f"""
|
|
1378
|
+
<img src="{img_url}" style="
|
|
1379
|
+
width:260px;height:340px;object-fit:cover;
|
|
1380
|
+
display:block;margin-top:32px;
|
|
1381
|
+
filter:grayscale(100%) contrast(1.1);"/>"""
|
|
1382
|
+
|
|
1383
|
+
meta_lines = []
|
|
1384
|
+
if t.get("author"):
|
|
1385
|
+
meta_lines.append(f'<div class="meta-line">{t["author"]}</div>')
|
|
1386
|
+
if t.get("subtitle"):
|
|
1387
|
+
meta_lines.append(f'<div class="meta-line meta-role">{t["subtitle"]}</div>')
|
|
1388
|
+
if t.get("date"):
|
|
1389
|
+
meta_lines.append(f'<div class="meta-line meta-date">{t["date"]}</div>')
|
|
1390
|
+
meta_block = "\n".join(meta_lines)
|
|
1391
|
+
|
|
1392
|
+
return f"""<!DOCTYPE html>
|
|
1393
|
+
<html>
|
|
1394
|
+
<head><meta charset="UTF-8">
|
|
1395
|
+
<style>
|
|
1396
|
+
{_base_css(t)}
|
|
1397
|
+
html, body {{ background: {bg}; }}
|
|
1398
|
+
.page {{ background: {bg}; }}
|
|
1399
|
+
|
|
1400
|
+
/* Left sidebar — the dominant color element */
|
|
1401
|
+
.sidebar {{
|
|
1402
|
+
position: absolute;
|
|
1403
|
+
top: 0; left: 0;
|
|
1404
|
+
width: {sidebar_w}px; height: 1123px;
|
|
1405
|
+
background: {accent};
|
|
1406
|
+
}}
|
|
1407
|
+
|
|
1408
|
+
/* Main content — offset from sidebar */
|
|
1409
|
+
.content {{
|
|
1410
|
+
position: absolute;
|
|
1411
|
+
left: {sidebar_w + 52}px; right: 52px;
|
|
1412
|
+
top: 100px; bottom: 80px;
|
|
1413
|
+
}}
|
|
1414
|
+
|
|
1415
|
+
/* Oversized display title */
|
|
1416
|
+
.title {{
|
|
1417
|
+
font-family: '{t['font_display']}', 'Arial Black', Impact, sans-serif;
|
|
1418
|
+
font-weight: 900;
|
|
1419
|
+
font-size: 96px;
|
|
1420
|
+
line-height: 0.92;
|
|
1421
|
+
color: {dark};
|
|
1422
|
+
letter-spacing: -0.03em;
|
|
1423
|
+
text-transform: uppercase;
|
|
1424
|
+
max-width: 620px;
|
|
1425
|
+
word-wrap: break-word;
|
|
1426
|
+
margin-bottom: 22px;
|
|
1427
|
+
}}
|
|
1428
|
+
|
|
1429
|
+
.subtitle {{
|
|
1430
|
+
font-family: '{t['font_body']}', 'Courier New', monospace;
|
|
1431
|
+
font-size: 12px;
|
|
1432
|
+
color: {muted};
|
|
1433
|
+
letter-spacing: 0.05em;
|
|
1434
|
+
margin-bottom: 0;
|
|
1435
|
+
}}
|
|
1436
|
+
|
|
1437
|
+
/* Thin rule under title area */
|
|
1438
|
+
.rule {{
|
|
1439
|
+
width: 64px; height: 2px;
|
|
1440
|
+
background: {dark};
|
|
1441
|
+
margin: 24px 0 28px;
|
|
1442
|
+
}}
|
|
1443
|
+
|
|
1444
|
+
/* Author / meta in typewriter font */
|
|
1445
|
+
.meta-group {{
|
|
1446
|
+
margin-top: 32px;
|
|
1447
|
+
}}
|
|
1448
|
+
.meta-line {{
|
|
1449
|
+
font-family: '{t['font_body']}', 'Courier New', monospace;
|
|
1450
|
+
font-size: 12px; color: {dark};
|
|
1451
|
+
line-height: 1.8; letter-spacing: 0.02em;
|
|
1452
|
+
}}
|
|
1453
|
+
.meta-role {{
|
|
1454
|
+
font-family: '{t['font_body']}', 'Courier New', monospace;
|
|
1455
|
+
color: {muted};
|
|
1456
|
+
}}
|
|
1457
|
+
.meta-date {{
|
|
1458
|
+
font-family: '{t['font_body']}', 'Courier New', monospace;
|
|
1459
|
+
font-size: 12px; color: {dark};
|
|
1460
|
+
margin-top: 8px;
|
|
1461
|
+
}}
|
|
1462
|
+
|
|
1463
|
+
/* Right-side content area for thumbnail */
|
|
1464
|
+
.right-col {{
|
|
1465
|
+
position: absolute;
|
|
1466
|
+
right: 52px;
|
|
1467
|
+
top: 380px; bottom: 80px;
|
|
1468
|
+
display: flex;
|
|
1469
|
+
flex-direction: column;
|
|
1470
|
+
align-items: flex-end;
|
|
1471
|
+
}}
|
|
1472
|
+
|
|
1473
|
+
/* Small accent square icon */
|
|
1474
|
+
.icon-block {{
|
|
1475
|
+
width: 64px; height: 64px;
|
|
1476
|
+
background: {accent};
|
|
1477
|
+
margin-top: 28px;
|
|
1478
|
+
display: flex; align-items: center; justify-content: center;
|
|
1479
|
+
flex-shrink: 0;
|
|
1480
|
+
}}
|
|
1481
|
+
.icon-lines {{
|
|
1482
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
1483
|
+
}}
|
|
1484
|
+
.icon-line {{
|
|
1485
|
+
height: 2px; background: {text_l};
|
|
1486
|
+
}}
|
|
1487
|
+
</style>
|
|
1488
|
+
</head>
|
|
1489
|
+
<body>
|
|
1490
|
+
<div class="page">
|
|
1491
|
+
<div class="sidebar"></div>
|
|
1492
|
+
|
|
1493
|
+
<div class="content">
|
|
1494
|
+
<div class="title">{t['title']}</div>
|
|
1495
|
+
{subtitle_block}
|
|
1496
|
+
<div class="rule"></div>
|
|
1497
|
+
<div class="meta-group">{meta_block}</div>
|
|
1498
|
+
</div>
|
|
1499
|
+
|
|
1500
|
+
<div class="right-col">
|
|
1501
|
+
{image_block}
|
|
1502
|
+
<div class="icon-block">
|
|
1503
|
+
<div class="icon-lines">
|
|
1504
|
+
<div class="icon-line" style="width:32px;"></div>
|
|
1505
|
+
<div class="icon-line" style="width:24px;"></div>
|
|
1506
|
+
<div class="icon-line" style="width:28px;"></div>
|
|
1507
|
+
</div>
|
|
1508
|
+
</div>
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
</body></html>"""
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
# ── Dispatch ───────────────────────────────────────────────────────────────────
|
|
1515
|
+
PATTERNS = {
|
|
1516
|
+
"fullbleed": _pattern_fullbleed,
|
|
1517
|
+
"split": _pattern_split,
|
|
1518
|
+
"typographic": _pattern_typographic,
|
|
1519
|
+
"atmospheric": _pattern_atmospheric,
|
|
1520
|
+
"minimal": _pattern_minimal,
|
|
1521
|
+
"stripe": _pattern_stripe,
|
|
1522
|
+
"diagonal": _pattern_diagonal,
|
|
1523
|
+
"frame": _pattern_frame,
|
|
1524
|
+
"editorial": _pattern_editorial,
|
|
1525
|
+
"magazine": _pattern_magazine,
|
|
1526
|
+
"darkroom": _pattern_darkroom,
|
|
1527
|
+
"terminal": _pattern_terminal,
|
|
1528
|
+
"poster": _pattern_poster,
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def render(tokens: dict) -> str:
|
|
1533
|
+
"""Dispatch to the cover pattern function and return the HTML string."""
|
|
1534
|
+
pattern = tokens.get("cover_pattern", "fullbleed")
|
|
1535
|
+
fn = PATTERNS.get(pattern, _pattern_fullbleed)
|
|
1536
|
+
return fn(tokens)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
# ── CLI ───────────────────────────────────────────────────────────────────────
|
|
1540
|
+
def main():
|
|
1541
|
+
"""CLI entry point."""
|
|
1542
|
+
parser = argparse.ArgumentParser(description="Render cover HTML from tokens.json")
|
|
1543
|
+
parser.add_argument("--tokens", default="tokens.json")
|
|
1544
|
+
parser.add_argument("--out", default="cover.html")
|
|
1545
|
+
parser.add_argument("--subtitle", default="", help="Optional subtitle override")
|
|
1546
|
+
args = parser.parse_args()
|
|
1547
|
+
|
|
1548
|
+
try:
|
|
1549
|
+
with open(args.tokens, encoding="utf-8") as f:
|
|
1550
|
+
tokens = json.load(f)
|
|
1551
|
+
except FileNotFoundError:
|
|
1552
|
+
print(json.dumps({"status": "error", "error": f"tokens file not found: {args.tokens}"}),
|
|
1553
|
+
file=sys.stderr)
|
|
1554
|
+
sys.exit(1)
|
|
1555
|
+
except json.JSONDecodeError as e:
|
|
1556
|
+
print(json.dumps({"status": "error", "error": f"invalid JSON: {e}"}), file=sys.stderr)
|
|
1557
|
+
sys.exit(1)
|
|
1558
|
+
|
|
1559
|
+
if args.subtitle:
|
|
1560
|
+
tokens["subtitle"] = args.subtitle
|
|
1561
|
+
|
|
1562
|
+
html = render(tokens)
|
|
1563
|
+
|
|
1564
|
+
try:
|
|
1565
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
1566
|
+
f.write(html)
|
|
1567
|
+
except OSError as e:
|
|
1568
|
+
print(json.dumps({"status": "error", "error": str(e)}), file=sys.stderr)
|
|
1569
|
+
sys.exit(3)
|
|
1570
|
+
|
|
1571
|
+
print(json.dumps({
|
|
1572
|
+
"status": "ok",
|
|
1573
|
+
"out": args.out,
|
|
1574
|
+
"pattern": tokens.get("cover_pattern"),
|
|
1575
|
+
}))
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
if __name__ == "__main__":
|
|
1579
|
+
main()
|