feat: paginate React cards like print
This commit is contained in:
@@ -143,6 +143,17 @@ rozmiaru:
|
||||
- `label` jest stabilnym identyfikatorem używanym przez `figure_refs`;
|
||||
- `full_size_link` domyślnie włącza otwieranie zrzutu HTML w pełnym rozmiarze.
|
||||
|
||||
Jeśli każda figura ma odpowiadać osobnej stronie wydruku, sekcja deklaruje
|
||||
`"asset_page_mode": "one-per-page"`. Generator wtedy wstawia jawne podziały
|
||||
stron w TeX i buduje osobne płótna A4 w React/HTML. Pierwsza strona zachowuje
|
||||
nagłówek sekcji, a kolejne są stronami kontynuacji z powtórzoną tabliczką
|
||||
zasobu. Widok ekranowy rozdziela płótna odstępem `5cm`; reguła nie wpływa na
|
||||
wydruk.
|
||||
|
||||
Końcowy słownik jest domyślnie obecny w React/HTML dla zgodności. Karta, która
|
||||
ma zawierać wyłącznie jawnie wymienione strony, ustawia na poziomie głównym
|
||||
`"render_dictionary": false`.
|
||||
|
||||
### Zadania
|
||||
|
||||
`tasks` jest słownikiem zadań, a `tasks_order` określa ich kolejność. Każde
|
||||
|
||||
+17
-12
@@ -163,6 +163,7 @@ type Section = {
|
||||
id: string;
|
||||
index: number;
|
||||
title: string;
|
||||
show_heading: boolean;
|
||||
header_html: string;
|
||||
left_margin_html: string;
|
||||
right_margin_html: string;
|
||||
@@ -182,7 +183,7 @@ type CardModel = {
|
||||
total_pages: number;
|
||||
front: any;
|
||||
sections: Section[];
|
||||
dictionary: { header_html: string; content_html: string };
|
||||
dictionary: { header_html: string; content_html: string } | null;
|
||||
toc: { id: string; label: string }[];
|
||||
};
|
||||
type NvimPanelSummary = {
|
||||
@@ -2714,10 +2715,12 @@ function SectionPage({
|
||||
className="section-sheet"
|
||||
>
|
||||
<div id={section.id} className="section-anchor" />
|
||||
<h2 className="section-heading">
|
||||
<span className="section-index">{section.index}</span>
|
||||
<span className="section-title">{section.title}</span>
|
||||
</h2>
|
||||
{section.show_heading && (
|
||||
<h2 className="section-heading">
|
||||
<span className="section-index">{section.index}</span>
|
||||
<span className="section-title">{section.title}</span>
|
||||
</h2>
|
||||
)}
|
||||
<StepMap steps={section.steps} progress={progress} onCycle={onCycle} />
|
||||
{section.content_html && <Html html={section.content_html} />}
|
||||
<div className="tasks">
|
||||
@@ -3117,13 +3120,15 @@ function App({ model }: { model: CardModel }) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Sheet
|
||||
header={model.dictionary.header_html}
|
||||
className="dictionary-sheet"
|
||||
>
|
||||
<h2>Słownik WE/EN/EK/KW</h2>
|
||||
<Html html={model.dictionary.content_html} />
|
||||
</Sheet>
|
||||
{model.dictionary && (
|
||||
<Sheet
|
||||
header={model.dictionary.header_html}
|
||||
className="dictionary-sheet"
|
||||
>
|
||||
<h2>Słownik WE/EN/EK/KW</h2>
|
||||
<Html html={model.dictionary.content_html} />
|
||||
</Sheet>
|
||||
)}
|
||||
</main>
|
||||
<ProgressDock progress={progress} error={progressError} />
|
||||
<ShortcutHelp open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
.paper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5cm;
|
||||
}
|
||||
.paper > .sheet {
|
||||
height: 297mm;
|
||||
min-height: 297mm;
|
||||
max-height: 297mm;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.paper > .sheet > .sheet-content {
|
||||
height: 297mm;
|
||||
min-height: 297mm;
|
||||
max-height: 297mm;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.viewer-chrome {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -1471,6 +1491,17 @@ text.uml-step-element-active {
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
.paper {
|
||||
display: block;
|
||||
gap: 0;
|
||||
}
|
||||
.paper > .sheet,
|
||||
.paper > .sheet > .sheet-content {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.viewer-chrome {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
"debug_checkpoints": {
|
||||
"$ref": "#/$defs/debugCheckpoints"
|
||||
},
|
||||
"render_dictionary": {
|
||||
"type": "boolean",
|
||||
"description": "Czy React/HTML ma dodać końcową stronę słownika WE/EN/EK/KW."
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
@@ -547,6 +551,10 @@
|
||||
"content_tex": {
|
||||
"type": "string"
|
||||
},
|
||||
"asset_page_mode": {
|
||||
"enum": ["flow", "one-per-page"],
|
||||
"description": "Sposób paginacji assetów. one-per-page wymusza osobną kartkę A4 w HTML i PDF."
|
||||
},
|
||||
"area_tree_refs": {
|
||||
"$ref": "#/$defs/stringList"
|
||||
},
|
||||
|
||||
+48
-11
@@ -6901,7 +6901,10 @@ def tex_asset_path(data: dict, asset_path: str) -> str:
|
||||
|
||||
def render_section_assets_tex(section: dict, data: dict) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for asset in section.get("assets", []) or []:
|
||||
one_per_page = section.get("asset_page_mode") == "one-per-page"
|
||||
for asset_index, asset in enumerate(section.get("assets", []) or []):
|
||||
if one_per_page and asset_index > 0:
|
||||
lines.append(r"\clearpage")
|
||||
path = tex_asset_path(data, str(asset.get("path", "")))
|
||||
width = float(asset.get("width", 1.0))
|
||||
caption = tex_escape(str(asset.get("caption", "")))
|
||||
@@ -7050,14 +7053,40 @@ def react_task_model(
|
||||
def react_card_model(data: dict) -> dict:
|
||||
equation_numbers, figure_numbers = collect_numbered_labels(data)
|
||||
sections = ordered_sections(data)
|
||||
page_specs: list[tuple[dict, str | None]] = []
|
||||
page_specs: list[dict] = []
|
||||
for section in sections:
|
||||
task_refs = section.get("task_refs", []) or []
|
||||
if section.get("content_kind") == "tasks" and task_refs:
|
||||
page_specs.extend((section, str(task_ref)) for task_ref in task_refs)
|
||||
page_specs.extend(
|
||||
{
|
||||
"section": section,
|
||||
"task_ref": str(task_ref),
|
||||
"asset_indexes": None,
|
||||
"include_body": True,
|
||||
}
|
||||
for task_ref in task_refs
|
||||
)
|
||||
elif section.get("asset_page_mode") == "one-per-page" and section.get("assets"):
|
||||
page_specs.extend(
|
||||
{
|
||||
"section": section,
|
||||
"task_ref": None,
|
||||
"asset_indexes": [asset_index],
|
||||
"include_body": asset_index == 0,
|
||||
}
|
||||
for asset_index, _asset in enumerate(section.get("assets", []) or [])
|
||||
)
|
||||
else:
|
||||
page_specs.append((section, None))
|
||||
total_pages = 2 + len(page_specs)
|
||||
page_specs.append(
|
||||
{
|
||||
"section": section,
|
||||
"task_ref": None,
|
||||
"asset_indexes": None,
|
||||
"include_body": True,
|
||||
}
|
||||
)
|
||||
include_dictionary = bool(data.get("render_dictionary", True))
|
||||
total_pages = 1 + len(page_specs) + int(include_dictionary)
|
||||
front_left, front_right = render_dual_margin_html(
|
||||
learning_tree_tags(data, column="ogolne", effect_color="green!50!black"),
|
||||
learning_tree_tags(data, column="zawodowe", effect_color="orange!85!black"),
|
||||
@@ -7092,7 +7121,10 @@ def react_card_model(data: dict) -> dict:
|
||||
],
|
||||
}
|
||||
section_models: list[dict] = []
|
||||
for index, (section, task_ref) in enumerate(page_specs, start=1):
|
||||
for index, page_spec in enumerate(page_specs, start=1):
|
||||
section = page_spec["section"]
|
||||
task_ref = page_spec["task_ref"]
|
||||
include_body = bool(page_spec["include_body"])
|
||||
left_margin, right_margin = render_section_margins_html(section, data)
|
||||
task_refs = section.get("task_refs", []) or []
|
||||
tasks = (
|
||||
@@ -7102,7 +7134,11 @@ def react_card_model(data: dict) -> dict:
|
||||
)
|
||||
assets = []
|
||||
include_section_assets = not task_ref or task_ref == str(task_refs[0])
|
||||
for asset in (section.get("assets", []) or []) if include_section_assets else []:
|
||||
section_assets = (section.get("assets", []) or []) if include_section_assets else []
|
||||
asset_indexes = page_spec["asset_indexes"]
|
||||
if asset_indexes is not None:
|
||||
section_assets = [section_assets[asset_index] for asset_index in asset_indexes]
|
||||
for asset in section_assets:
|
||||
source_path = str(asset.get("html_path") or asset.get("path", ""))
|
||||
interactive = copy.deepcopy(asset.get("interactive"))
|
||||
if interactive:
|
||||
@@ -7137,11 +7173,12 @@ def react_card_model(data: dict) -> dict:
|
||||
"id": html_id(f'section-{index}-{task_ref or "content"}'),
|
||||
"index": index,
|
||||
"title": title,
|
||||
"show_heading": include_body,
|
||||
"header_html": render_page_header_html(data, index + 1, total_pages),
|
||||
"left_margin_html": left_margin,
|
||||
"right_margin_html": right_margin,
|
||||
"content_html": ""
|
||||
if section.get("content_kind") == "tasks"
|
||||
if section.get("content_kind") == "tasks" or not include_body
|
||||
else render_latex_fragment_html(
|
||||
str(section.get("content_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
@@ -7150,7 +7187,7 @@ def react_card_model(data: dict) -> dict:
|
||||
"steps": [
|
||||
{"id": str(step.get("id", "")), "title": str(step.get("title", ""))}
|
||||
for step in section.get("steps", []) or []
|
||||
] if (not task_ref or task_ref == str(task_refs[0])) else [],
|
||||
] if include_body and (not task_ref or task_ref == str(task_refs[0])) else [],
|
||||
}
|
||||
)
|
||||
return {
|
||||
@@ -7162,10 +7199,10 @@ def react_card_model(data: dict) -> dict:
|
||||
"dictionary": {
|
||||
"header_html": render_page_header_html(data, total_pages, total_pages),
|
||||
"content_html": render_learning_tree_overview_html(data),
|
||||
},
|
||||
} if include_dictionary else None,
|
||||
"toc": [
|
||||
{"id": section["id"], "label": f'{section["index"]}. {section["title"]}'}
|
||||
for section in section_models
|
||||
for section in section_models if section["show_heading"]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user