From 0816f270539f603014adeef8625d2d5edcb3bcac Mon Sep 17 00:00:00 2001
From: user
Date: Fri, 17 Jul 2026 09:49:21 +0200
Subject: [PATCH] feat: paginate React cards like print
---
docs/CARD.md | 11 ++++++
react/src/main.tsx | 29 +++++++++-------
react/src/react.css | 31 +++++++++++++++++
schemas/card-source.schema.json | 8 +++++
tools/render_card.py | 59 +++++++++++++++++++++++++++------
5 files changed, 115 insertions(+), 23 deletions(-)
diff --git a/docs/CARD.md b/docs/CARD.md
index 81ee67b..a1c766d 100644
--- a/docs/CARD.md
+++ b/docs/CARD.md
@@ -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
diff --git a/react/src/main.tsx b/react/src/main.tsx
index 15e23c4..de1c942 100644
--- a/react/src/main.tsx
+++ b/react/src/main.tsx
@@ -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"
>
-
- {section.index}
- {section.title}
-
+ {section.show_heading && (
+
+ {section.index}
+ {section.title}
+
+ )}
{section.content_html && }
@@ -3117,13 +3120,15 @@ function App({ model }: { model: CardModel }) {
/>
);
})}
-
- Słownik WE/EN/EK/KW
-
-
+ {model.dictionary && (
+
+ Słownik WE/EN/EK/KW
+
+
+ )}
setShortcutsOpen(false)} />
diff --git a/react/src/react.css b/react/src/react.css
index 89ac63d..3778a07 100644
--- a/react/src/react.css
+++ b/react/src/react.css
@@ -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;
}
diff --git a/schemas/card-source.schema.json b/schemas/card-source.schema.json
index 062f669..73c8004 100644
--- a/schemas/card-source.schema.json
+++ b/schemas/card-source.schema.json
@@ -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"
},
diff --git a/tools/render_card.py b/tools/render_card.py
index 7feaa02..0d2412d 100644
--- a/tools/render_card.py
+++ b/tools/render_card.py
@@ -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"]
],
}