feat: paginate React cards like print

This commit is contained in:
user
2026-07-17 09:49:21 +02:00
parent b8b122abb9
commit 0816f27053
5 changed files with 115 additions and 23 deletions
+11
View File
@@ -143,6 +143,17 @@ rozmiaru:
- `label` jest stabilnym identyfikatorem używanym przez `figure_refs`; - `label` jest stabilnym identyfikatorem używanym przez `figure_refs`;
- `full_size_link` domyślnie włącza otwieranie zrzutu HTML w pełnym rozmiarze. - `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 ### Zadania
`tasks` jest słownikiem zadań, a `tasks_order` określa ich kolejność. Każde `tasks` jest słownikiem zadań, a `tasks_order` określa ich kolejność. Każde
+6 -1
View File
@@ -163,6 +163,7 @@ type Section = {
id: string; id: string;
index: number; index: number;
title: string; title: string;
show_heading: boolean;
header_html: string; header_html: string;
left_margin_html: string; left_margin_html: string;
right_margin_html: string; right_margin_html: string;
@@ -182,7 +183,7 @@ type CardModel = {
total_pages: number; total_pages: number;
front: any; front: any;
sections: Section[]; sections: Section[];
dictionary: { header_html: string; content_html: string }; dictionary: { header_html: string; content_html: string } | null;
toc: { id: string; label: string }[]; toc: { id: string; label: string }[];
}; };
type NvimPanelSummary = { type NvimPanelSummary = {
@@ -2714,10 +2715,12 @@ function SectionPage({
className="section-sheet" className="section-sheet"
> >
<div id={section.id} className="section-anchor" /> <div id={section.id} className="section-anchor" />
{section.show_heading && (
<h2 className="section-heading"> <h2 className="section-heading">
<span className="section-index">{section.index}</span> <span className="section-index">{section.index}</span>
<span className="section-title">{section.title}</span> <span className="section-title">{section.title}</span>
</h2> </h2>
)}
<StepMap steps={section.steps} progress={progress} onCycle={onCycle} /> <StepMap steps={section.steps} progress={progress} onCycle={onCycle} />
{section.content_html && <Html html={section.content_html} />} {section.content_html && <Html html={section.content_html} />}
<div className="tasks"> <div className="tasks">
@@ -3117,6 +3120,7 @@ function App({ model }: { model: CardModel }) {
/> />
); );
})} })}
{model.dictionary && (
<Sheet <Sheet
header={model.dictionary.header_html} header={model.dictionary.header_html}
className="dictionary-sheet" className="dictionary-sheet"
@@ -3124,6 +3128,7 @@ function App({ model }: { model: CardModel }) {
<h2>Słownik WE/EN/EK/KW</h2> <h2>Słownik WE/EN/EK/KW</h2>
<Html html={model.dictionary.content_html} /> <Html html={model.dictionary.content_html} />
</Sheet> </Sheet>
)}
</main> </main>
<ProgressDock progress={progress} error={progressError} /> <ProgressDock progress={progress} error={progressError} />
<ShortcutHelp open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} /> <ShortcutHelp open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
+31
View File
@@ -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 { .viewer-chrome {
position: sticky; position: sticky;
top: 0; top: 0;
@@ -1471,6 +1491,17 @@ text.uml-step-element-active {
} }
} }
@media print { @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 { .viewer-chrome {
display: none !important; display: none !important;
} }
+8
View File
@@ -31,6 +31,10 @@
"debug_checkpoints": { "debug_checkpoints": {
"$ref": "#/$defs/debugCheckpoints" "$ref": "#/$defs/debugCheckpoints"
}, },
"render_dictionary": {
"type": "boolean",
"description": "Czy React/HTML ma dodać końcową stronę słownika WE/EN/EK/KW."
},
"template": { "template": {
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
@@ -547,6 +551,10 @@
"content_tex": { "content_tex": {
"type": "string" "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": { "area_tree_refs": {
"$ref": "#/$defs/stringList" "$ref": "#/$defs/stringList"
}, },
+48 -11
View File
@@ -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]: def render_section_assets_tex(section: dict, data: dict) -> list[str]:
lines: 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", ""))) path = tex_asset_path(data, str(asset.get("path", "")))
width = float(asset.get("width", 1.0)) width = float(asset.get("width", 1.0))
caption = tex_escape(str(asset.get("caption", ""))) caption = tex_escape(str(asset.get("caption", "")))
@@ -7050,14 +7053,40 @@ def react_task_model(
def react_card_model(data: dict) -> dict: def react_card_model(data: dict) -> dict:
equation_numbers, figure_numbers = collect_numbered_labels(data) equation_numbers, figure_numbers = collect_numbered_labels(data)
sections = ordered_sections(data) sections = ordered_sections(data)
page_specs: list[tuple[dict, str | None]] = [] page_specs: list[dict] = []
for section in sections: for section in sections:
task_refs = section.get("task_refs", []) or [] task_refs = section.get("task_refs", []) or []
if section.get("content_kind") == "tasks" and task_refs: 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: else:
page_specs.append((section, None)) page_specs.append(
total_pages = 2 + len(page_specs) {
"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( front_left, front_right = render_dual_margin_html(
learning_tree_tags(data, column="ogolne", effect_color="green!50!black"), learning_tree_tags(data, column="ogolne", effect_color="green!50!black"),
learning_tree_tags(data, column="zawodowe", effect_color="orange!85!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] = [] 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) left_margin, right_margin = render_section_margins_html(section, data)
task_refs = section.get("task_refs", []) or [] task_refs = section.get("task_refs", []) or []
tasks = ( tasks = (
@@ -7102,7 +7134,11 @@ def react_card_model(data: dict) -> dict:
) )
assets = [] assets = []
include_section_assets = not task_ref or task_ref == str(task_refs[0]) 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", "")) source_path = str(asset.get("html_path") or asset.get("path", ""))
interactive = copy.deepcopy(asset.get("interactive")) interactive = copy.deepcopy(asset.get("interactive"))
if interactive: if interactive:
@@ -7137,11 +7173,12 @@ def react_card_model(data: dict) -> dict:
"id": html_id(f'section-{index}-{task_ref or "content"}'), "id": html_id(f'section-{index}-{task_ref or "content"}'),
"index": index, "index": index,
"title": title, "title": title,
"show_heading": include_body,
"header_html": render_page_header_html(data, index + 1, total_pages), "header_html": render_page_header_html(data, index + 1, total_pages),
"left_margin_html": left_margin, "left_margin_html": left_margin,
"right_margin_html": right_margin, "right_margin_html": right_margin,
"content_html": "" "content_html": ""
if section.get("content_kind") == "tasks" if section.get("content_kind") == "tasks" or not include_body
else render_latex_fragment_html( else render_latex_fragment_html(
str(section.get("content_tex", "")), equation_numbers, figure_numbers str(section.get("content_tex", "")), equation_numbers, figure_numbers
), ),
@@ -7150,7 +7187,7 @@ def react_card_model(data: dict) -> dict:
"steps": [ "steps": [
{"id": str(step.get("id", "")), "title": str(step.get("title", ""))} {"id": str(step.get("id", "")), "title": str(step.get("title", ""))}
for step in section.get("steps", []) or [] 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 { return {
@@ -7162,10 +7199,10 @@ def react_card_model(data: dict) -> dict:
"dictionary": { "dictionary": {
"header_html": render_page_header_html(data, total_pages, total_pages), "header_html": render_page_header_html(data, total_pages, total_pages),
"content_html": render_learning_tree_overview_html(data), "content_html": render_learning_tree_overview_html(data),
}, } if include_dictionary else None,
"toc": [ "toc": [
{"id": section["id"], "label": f'{section["index"]}. {section["title"]}'} {"id": section["id"], "label": f'{section["index"]}. {section["title"]}'}
for section in section_models for section in section_models if section["show_heading"]
], ],
} }