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`;
|
- `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
@@ -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)} />
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user