diff --git a/docs/CARD.md b/docs/CARD.md index b407942..41eae31 100644 --- a/docs/CARD.md +++ b/docs/CARD.md @@ -23,6 +23,12 @@ Obiekt `card` identyfikuje serię, numer, wersję, UUID, autora i status. Obiekt `generated` wskazuje ścieżki wyjściowe TeX/HTML/CSS. Pole `template` wybiera layout. +Opcjonalny obiekt `title_block` dodaje do pierwszego nagłówka tekstową +tabliczkę dokumentu: osoby odpowiedzialne, kategorię, tytuł, URL, rewizję, +datę wydania, serię, typ, narzędzie i arkusz. Pole `url` jest kodowane także +lokalnie w QR (SVG w HTML i pakiet `qrcode` w TeX). Gdy URL nie jest podany, +generator pokazuje w tej komórce informację `brak URL`. + ### Efekty i kryteria - `educational_requirements` — wymagania `WE`; @@ -49,6 +55,11 @@ Layouty 5a/5b obsługują również: - `code_block.blocks[].web_steps` — interaktywne kroki HTML; - `hardware_procedure[]` — tabela `step/action/condition` w TeX i HTML. +`code_block.blocks[]` jest rzeczywistym podziałem na bloki: każdy wpis jest +renderowany jako osobna, opisana ramka w sekcji implementacyjnej, a jego +`web_steps` zasila niezależną checklistę HTML. Kolejność głównych bloków +strony jest opisana przez `block_map` wybranego szablonu. + ### Zadania `tasks` jest słownikiem zadań, a `tasks_order` określa ich kolejność. Każde diff --git a/examples/card/json/card_source.json b/examples/card/json/card_source.json index 5082596..1186a2f 100644 --- a/examples/card/json/card_source.json +++ b/examples/card/json/card_source.json @@ -26,6 +26,22 @@ "html_css": "build/html/style.css" }, "template": "templates/karta-5b.json", + "title_block": { + "category": "KARTA PRACY · INFORMATYKA", + "standard": "ISO 7200", + "prepared_by": "Zespół edukacyjny", + "prepared_on": "15.07.2026", + "checked_by": "—", + "approved_by": "—", + "url": "https://demo.example.edu/cards/demo-01", + "revision": "1", + "issued_on": "15.07.2026", + "series": "DEMO-01", + "document_type": "karta pracy", + "tool": "card-layouts", + "sheet": "1 / 11", + "show_qr": true + }, "front_page_break": false, "metric_fill_fields": [ { diff --git a/requirements-dev.txt b/requirements-dev.txt index f3ad13d..58598fb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,2 @@ jsonschema>=4,<5 +reportlab>=3.6,<5 diff --git a/schemas/card-source.schema.json b/schemas/card-source.schema.json index 0bc8dc6..b9d11e6 100644 --- a/schemas/card-source.schema.json +++ b/schemas/card-source.schema.json @@ -32,6 +32,9 @@ "type": "string", "minLength": 1 }, + "title_block": { + "$ref": "#/$defs/titleBlock" + }, "dictionary_refs": { "type": "object", "properties": { @@ -228,6 +231,31 @@ }, "additionalProperties": false }, + "titleBlock": { + "type": "object", + "description": "Tekstowa tabliczka w nagłówku: metadane dokumentu oraz opcjonalny adres kodowany w QR.", + "properties": { + "category": { "type": "string" }, + "standard": { "type": "string" }, + "prepared_by": { "type": "string" }, + "prepared_on": { "type": "string" }, + "checked_by": { "type": "string" }, + "approved_by": { "type": "string" }, + "title": { "type": "string" }, + "url": { "type": "string" }, + "url_host_uuid": { "type": "string" }, + "url_port": { "type": ["string", "integer"] }, + "doc_uuid": { "type": "string" }, + "revision": { "type": "string" }, + "issued_on": { "type": "string" }, + "series": { "type": "string" }, + "document_type": { "type": "string" }, + "tool": { "type": "string" }, + "sheet": { "type": "string" }, + "show_qr": { "type": "boolean" } + }, + "additionalProperties": false + }, "sideMarginLayout": { "type": "object", "required": ["columns"], diff --git a/scripts/check_repository.py b/scripts/check_repository.py index 6b169db..52d4eba 100644 --- a/scripts/check_repository.py +++ b/scripts/check_repository.py @@ -81,12 +81,18 @@ def check_render() -> None: tex = tex_path.read_text(encoding="utf-8") html = html_path.read_text(encoding="utf-8") - for marker in ("DEMO-01", "IP WO", "INF04"): + for marker in ("DEMO-01", "IP WO", "INF04", "KARTA PRACY"): if marker not in tex: raise AssertionError(f"TeX nie zawiera znacznika {marker!r}") if marker not in html: raise AssertionError(f"HTML nie zawiera znacznika {marker!r}") + if r"\qrcode[height=" not in tex: + raise AssertionError("TeX nie zawiera kodu QR z tabliczki") + for marker in ('class="title-qr"', 'class="code-subblock"'): + if marker not in html: + raise AssertionError(f"HTML nie zawiera znacznika {marker!r}") + def main() -> int: check_json_files() diff --git a/templates/karta-5a.json b/templates/karta-5a.json index 49f14a3..b317ba4 100644 --- a/templates/karta-5a.json +++ b/templates/karta-5a.json @@ -3,8 +3,8 @@ "schema": "esc-card-template.v1", "id": "karta-5a", "name": "Karta 5a", - "version": "1.0", - "description": "Wielostronicowy layout A4 landscape wyekstrahowany z referencyjnego pliku series/esc/html/ESC-04 karta.html; pierwsze dwie strony zachowują konwencję wzorca, dalsze strony dokładają pełną treść karty.", + "version": "1.1", + "description": "Wielostronicowy layout A4 landscape wyekstrahowany z referencyjnego pliku series/esc/html/ESC-04 karta.html; pierwsze dwie strony zachowują konwencję wzorca, dalsze strony dokładają pełną treść karty. Wersja 1.1 dodaje tekstową tabliczkę ISO 7200 i odrębne ramki bloków kodu.", "tokens": { "page": { "format": "a4", @@ -61,13 +61,17 @@ }, "metric_header": { "border": "1.5px solid #1c1c1a", - "title_bar": true, + "title_block": "ISO 7200: odpowiedzialni, tytuł, URL+QR, rewizja, data, seria, typ, narzędzie, arkusz", "grid": "2x2", "fill_fields": [ "Uczeń", "Klasa · data" ] }, + "code_content_block": { + "border": "1px solid #d8d6d0; border-left: 3px solid #3b5a8f", + "source": "code_block.blocks[]" + }, "margin_tree_panel": { "left": "TECH — zawodowe; WE → EK → KW", "right": "OG — ogólne; WE → EN → KW", diff --git a/templates/karta-5b.json b/templates/karta-5b.json index 4a02cd2..ff64733 100644 --- a/templates/karta-5b.json +++ b/templates/karta-5b.json @@ -3,9 +3,9 @@ "schema": "esc-card-template.v1", "id": "karta-5b", "name": "Karta 5b", - "version": "1.3", - "approved": "04.07.2026 — layout referencyjny zatwierdzony przez prowadzącego (symetria 0,55/2,05/0,55/14,70; gołe drzewka); v1.1 — nagłówki punktowe zamiast §, 05.07.2026; v1.2 — linie nagłówka/stopki jako warstwa strony ograniczona do bloku treści, 06.07.2026; v1.3 — margines gorny/dolny 2,5 cm, 06.07.2026", - "description": "Wielostronicowy layout A4 portrait: charakter 5a z ramkami sekcji, ale z golymi drzewkami WE->EN/EK jako marginalia bez paneli i bez luznych KW. Linie strony sa warstwa overlay i nie wychodza pod drzewka. Blok tresci zaczyna sie 2,5 cm od gornej krawedzi i konczy 2,5 cm od dolnej.", + "version": "1.4", + "approved": "04.07.2026 — layout referencyjny zatwierdzony przez prowadzącego (symetria 0,55/2,05/0,55/14,70; gołe drzewka); v1.1 — nagłówki punktowe zamiast §, 05.07.2026; v1.2 — linie nagłówka/stopki jako warstwa strony ograniczona do bloku treści, 06.07.2026; v1.3 — margines gorny/dolny 2,5 cm, 06.07.2026; v1.4 — tekstowa tabliczka ISO 7200 + QR oraz ramki bloków kodu, 15.07.2026", + "description": "Wielostronicowy layout A4 portrait: charakter 5a z ramkami sekcji, ale z golymi drzewkami WE->EN/EK jako marginalia bez paneli i bez luznych KW. Linie strony sa warstwa overlay i nie wychodza pod drzewka. Blok tresci zaczyna sie 2,5 cm od gornej krawedzi i konczy 2,5 cm od dolnej. Wersja 1.4 ma tabliczkę dokumentu w nagłówku.", "tokens": { "page": { "format": "a4", @@ -89,13 +89,17 @@ }, "metric_header": { "border": "1.5px solid #1c1c1a", - "title_bar": true, + "title_block": "ISO 7200: odpowiedzialni, tytuł, URL+QR, rewizja, data, seria, typ, narzędzie, arkusz", "grid": "2x2", "fill_fields": [ "Uczen", "Klasa · data" ] }, + "code_content_block": { + "border": "1px solid #d8d6d0; border-left: 3px solid #3b5a8f", + "source": "code_block.blocks[]" + }, "margin_tree_panel": { "left": "TECH jako gole drzewko tekstowe WE->EK/EN bez KW, bez ramki, tla, naglowka i stopki", "right": "OG jako gole drzewko tekstowe WE->EK/EN bez KW, bez ramki, tla, naglowka i stopki", @@ -144,15 +148,15 @@ "block_map": { "page_1": [ "metric_header", - "mission", "objectives", - "agenda", - "hw_gate", - "theory_refs", - "tech_stack" + "agenda" ], "page_2": [ "running_card_header", + "mission", + "theory_refs", + "tech_stack", + "hw_gate", "math_block", "part_a_banner", "model_block", diff --git a/tools/render_card.py b/tools/render_card.py index 6ddcad3..aa7a9a2 100644 --- a/tools/render_card.py +++ b/tools/render_card.py @@ -2484,6 +2484,103 @@ def card_prefix(card: dict) -> str: return f"{series}-{number}" if number else series +def publication_url(data: dict) -> str: + """Return the canonical publication URL used by the title block and QR.""" + title_block = data.get("title_block") or {} + card = data.get("card") or {} + for value in ( + title_block.get("url"), + data.get("publication_url"), + card.get("publication_url"), + card.get("url"), + ): + if value: + return str(value).strip() + + host = title_block.get("url_host_uuid") or data.get("url_host_uuid") + port = title_block.get("url_port") or data.get("url_port") + document = title_block.get("doc_uuid") or data.get("doc_uuid") or card.get("uuid") + if host and document: + authority = str(host).strip() + if port: + authority += f":{str(port).strip()}" + return f"https://{authority}/{str(document).strip()}" + return "" + + +def title_block_fields(data: dict) -> dict[str, str | bool]: + """Resolve the ISO-7200-like header while keeping card metadata canonical.""" + card = data["card"] + configured = data.get("title_block") or {} + url = publication_url(data) + + def value(name: str, default: object = "") -> str: + current = configured.get(name, default) + return str(current).strip() if current is not None else "" + + revision_date = str(card.get("revision_date", "")).strip() + return { + "category": value("category", f"KARTA PRACY · {card.get('subject', '')}").upper(), + "standard": value("standard", "ISO 7200"), + "prepared_by": value("prepared_by", card.get("author", "—")) or "—", + "prepared_on": value("prepared_on", revision_date), + "checked_by": value("checked_by", "—") or "—", + "approved_by": value("approved_by", "—") or "—", + "title": value("title", card.get("title", "")), + "url": url, + "revision": value("revision", card.get("version", "")), + "issued_on": value("issued_on", revision_date), + "series": value("series", card_prefix(card)), + "document_type": value("document_type", "karta pracy"), + "tool": value("tool", "card-layouts"), + "sheet": value("sheet", "1 / 1"), + "show_qr": bool(configured.get("show_qr", bool(url))), + } + + +@lru_cache(maxsize=32) +def qr_svg(value: str) -> str: + """Generate a self-contained QR SVG without a network dependency. + + ReportLab ships a standards-compliant QR encoder on the supported + workstation image. The TeX renderer uses its native qrcode package; + HTML receives the same payload as inline SVG so the generated card is + fully portable. + """ + if not value: + return "" + try: + from reportlab.graphics.barcode.qr import QrCodeWidget + except ImportError: + return "" + + widget = QrCodeWidget(value=value, barLevel="L") + widget.qr.make() + modules = widget.qr.modules + size = len(modules) + border = 4 + path_parts: list[str] = [] + for row_index, row in enumerate(modules): + column = 0 + while column < size: + if not row[column]: + column += 1 + continue + start = column + while column < size and row[column]: + column += 1 + length = column - start + path_parts.append(f"M{start + border} {row_index + border}h{length}v1h-{length}z") + full_size = size + border * 2 + return ( + f'' + '' + f'' + "" + ) + + def margin_code_5a(prefix: str, official: str, local: str) -> str: if prefix == "WE": return " ".join(part for part in ("WE", official) if part) @@ -2599,6 +2696,53 @@ def render_5a_section_header(number: int, title: str) -> str: ) +def render_title_block_html(data: dict) -> str: + fields = title_block_fields(data) + + def cell(label: str, value: object, extra_class: str = "") -> str: + classes = "title-cell" + (f" {extra_class}" if extra_class else "") + shown = html_escape(str(value)) if value else "—" + return f'
{html_escape(label)}{shown}
' + + qr = qr_svg(str(fields["url"])) if fields["show_qr"] else "" + qr_cell = ( + f'{qr}' + if qr + else '
QR' + + ("brak modułu QR" if fields["url"] else "brak URL") + + "
" + ) + prepared = ( + '
Opracował' + f'{html_escape(str(fields["prepared_by"]))}' + f'{html_escape(str(fields["prepared_on"]))}
' + ) + return ( + '
' + '
' + f'{prepared}' + f'{cell("Sprawdził", fields["checked_by"], "signer")}' + f'{cell("Zatwierdził", fields["approved_by"], "signer")}' + '
' + '
' + f'
{html_escape(str(fields["category"]))}{html_escape(str(fields["standard"]))}
' + f'
Tytuł{html_escape(str(fields["title"]))}
' + f'
URL{html_escape(str(fields["url"])) if fields["url"] else "—"}
' + '
' + '
' + f'{cell("Rewizja", fields["revision"])}' + f'{cell("Data wydania", fields["issued_on"])}' + f'{cell("Seria", fields["series"])}' + f'{cell("Typ", fields["document_type"])}' + f'{cell("Narzędzie", fields["tool"])}' + f'{cell("Arkusz", fields["sheet"])}' + '
' + f'{qr_cell}' + '
' + ) + + def render_metric_header_5a_html(data: dict) -> str: card = data["card"] fields = data.get("metric_fill_fields") or [ @@ -2609,10 +2753,7 @@ def render_metric_header_5a_html(data: dict) -> str: second_label = fields[1].get("label", "Klasa · data") if len(fields) > 1 else "Klasa · data" return ( '
' - '
' - f'
{html_escape(card_title_line_5a(card))}
' - f'KARTA {html_escape(card["number"])}/{html_escape(card["count"])}' - '
' + f'{render_title_block_html(data)}' '
' f'
Projekt{html_escape(card["project"])}
' f'
Przedmiot{html_escape(card["subject"])}
' @@ -2813,12 +2954,12 @@ def render_code_subblock_5a_html(block: dict) -> str: if block.get("pass_condition"): pass_line = f'

PASS: {html_escape(text_5a(block.get("pass_condition", "")))}

' return ( - '
' + f'
' f'

{html_escape(text_5a(block.get("title", "")))}

' f'

{html_escape(text_5a(block.get("context", "")))}

' f'{render_code_columns_5a_html(block)}' f'{pass_line}' - '
' + '' ) @@ -3288,7 +3429,7 @@ body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items: .kw-common{border-top:1px solid #e4e2dc;margin-top:2px;padding-top:3px;font:400 7px/1.6 'JetBrains Mono',monospace;color:#8a887f}.kw-common span{color:#1c1c1a} .margin-panel-foot{padding:4px 8px;border-top:1px solid #d8d6d0;font:400 7px 'JetBrains Mono',monospace;color:#8a887f} .page-main{display:flex;flex-direction:column;min-height:0} -.metric-box{border:1.5px solid #1c1c1a}.metric-title{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:9px 14px;border-bottom:1px solid #1c1c1a}.metric-title div{font-size:15px;font-weight:700}.metric-title span{font:500 9.5px 'JetBrains Mono',monospace;color:#4a4944;white-space:nowrap} +.metric-box{border:1.5px solid #1c1c1a}.title-block{display:grid;grid-template-columns:1.08fr 2.55fr 1.25fr 66px;min-height:96px;border-bottom:1px solid #1c1c1a}.title-signers,.title-document{display:grid;grid-template-rows:repeat(3,minmax(0,1fr));min-width:0}.title-signers{border-right:1px solid #1c1c1a}.title-document{border-right:1px solid #1c1c1a}.title-meta{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:repeat(3,minmax(0,1fr));min-width:0}.title-cell,.title-name,.title-url{display:flex;flex-direction:column;justify-content:center;min-width:0;padding:3px 6px;border-bottom:1px solid #d8d6d0}.title-signers .title-cell:last-child,.title-document>div:last-child,.title-meta .title-cell:nth-last-child(-n+2){border-bottom:0}.title-meta .title-cell:nth-child(odd){border-right:1px solid #d8d6d0}.title-cell span,.title-name span,.title-url span{font:600 6.7px/1.15 'JetBrains Mono',monospace;letter-spacing:.08em;text-transform:uppercase;color:#8a887f}.title-cell strong,.title-name strong{font-size:8.3px;line-height:1.13;font-weight:500;overflow-wrap:anywhere}.title-cell em{font:400 6.4px/1.1 'JetBrains Mono',monospace;color:#8a887f;font-style:normal}.title-category{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:2px 6px;background:#3b5a8f;color:#fefefc;font-size:7.1px;letter-spacing:.06em;overflow:hidden}.title-category strong{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.title-category span{font:400 6.1px 'JetBrains Mono',monospace;white-space:nowrap;color:#fefefc}.title-name strong{font-size:9.2px;font-weight:700}.title-url code{font:400 6.4px/1.15 'JetBrains Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#4a4944}.title-qr-cell{display:flex;align-items:center;justify-content:center;padding:5px;border-left:1px solid #1c1c1a;background:#fefefc}.title-qr{width:100%;height:100%;display:block}.title-qr-empty{flex-direction:column;text-align:center;color:#8a887f}.title-qr-empty span{font:600 7px 'JetBrains Mono',monospace}.title-qr-empty strong{font:400 6px 'JetBrains Mono',monospace}.metric-title{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:9px 14px;border-bottom:1px solid #1c1c1a}.metric-title div{font-size:15px;font-weight:700}.metric-title span{font:500 9.5px 'JetBrains Mono',monospace;color:#4a4944;white-space:nowrap} .metric-grid{display:grid;grid-template-columns:1fr 1fr;font-size:11px}.metric-grid div{display:flex;gap:8px;padding:4px 14px;border-bottom:1px solid #d8d6d0}.metric-grid div:nth-child(odd){border-right:1px solid #d8d6d0}.metric-grid div:nth-last-child(-n+2){border-bottom:0}.metric-grid span{font:500 8.5px/1.9 'JetBrains Mono',monospace;color:#8a887f;text-transform:uppercase;flex:none}.metric-grid strong{font-weight:400}.metric-grid i{flex:1;border-bottom:1px dotted #9a988f} .plain-box{border:1px solid #1c1c1a;padding:8px 14px;margin-top:11px}.plain-title{font-weight:700;font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;margin-bottom:2px}.plain-box p{margin:0;text-align:justify} .section-box{border:1px solid #1c1c1a;margin-top:10px}.section-box:first-child{margin-top:0}.section-box.dense{margin-top:8px}.section-head{display:flex;align-items:baseline;gap:8px;padding:5px 14px;background:#f4f3ef;border-bottom:1px solid #1c1c1a}.section-head span{font:600 9px 'JetBrains Mono',monospace;color:#8a887f}.section-head strong{font-weight:700;font-size:11.5px;letter-spacing:.12em;text-transform:uppercase}.section-body{padding:7px 14px}.dense .section-body{padding:6px 12px} @@ -3300,7 +3441,7 @@ body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items: .theory-table{width:100%;border-collapse:collapse;font-size:11px}.theory-table th{text-align:left;padding:3px 10px;border:1px solid #1c1c1a;background:#f4f3ef}.theory-table th:first-child{width:26%}.theory-table td{padding:3px 10px;vertical-align:top;border:1px solid #d8d6d0}.theory-table code{font:400 10px 'JetBrains Mono',monospace} .tech-stack{display:grid;grid-template-columns:1fr 1fr;column-gap:18px;align-items:start}.stack-column h3{font:600 8.5px 'JetBrains Mono',monospace;letter-spacing:.08em;color:#4a4944;text-transform:uppercase;padding-bottom:3px;border-bottom:1px solid #1c1c1a;margin:0}.stack-grid{display:grid;grid-template-columns:88px 1fr;font-size:10px;line-height:1.35}.stack-grid div{padding:3px 0;border-bottom:1px solid #e4e2dc}.stack-grid div:nth-last-child(-n+2){border-bottom:0}.stack-grid code{font:400 9.5px/1.45 'JetBrains Mono',monospace} .part-banner{border:1.5px solid #1c1c1a;background:#f4f3ef;padding:7px 14px;display:grid;grid-template-columns:190px 1fr;column-gap:12px;align-items:baseline}.part-banner strong{font:700 12px 'JetBrains Mono',monospace;letter-spacing:.08em}.part-banner span{font-size:12px;text-align:justify} -.compact-list{margin:3px 0 0 15px;padding:0;font-size:10.5px;line-height:1.32}.compact-list li{margin:1px 0}.three-col{display:grid;grid-template-columns:1fr 1fr 1fr;column-gap:12px}.three-col h3,.model-grid h3{font:600 8.5px 'JetBrains Mono',monospace;text-transform:uppercase;color:#4a4944;margin:0 0 3px}.three-col p,.model-grid p{margin:0 0 4px;font-size:10.5px}.pass-line{margin:5px 0 0;padding-top:4px;border-top:1px solid #e4e2dc;font-size:10.5px} +.compact-list{margin:3px 0 0 15px;padding:0;font-size:10.5px;line-height:1.32}.compact-list li{margin:1px 0}.code-subblock{border:1px solid #d8d6d0;border-left:3px solid #3b5a8f;padding:5px 7px;margin-top:5px}.code-subblock:first-child{margin-top:0}.code-subblock h3{margin:0 0 2px}.code-subblock>p{margin:0 0 4px}.three-col{display:grid;grid-template-columns:1fr 1fr 1fr;column-gap:12px}.three-col h3,.model-grid h3{font:600 8.5px 'JetBrains Mono',monospace;text-transform:uppercase;color:#4a4944;margin:0 0 3px}.three-col p,.model-grid p{margin:0 0 4px;font-size:10.5px}.pass-line{margin:5px 0 0;padding-top:4px;border-top:1px solid #e4e2dc;font-size:10.5px} .math-task{display:grid;grid-template-columns:1fr 190px;column-gap:12px;align-items:start;border-bottom:1px solid #f0efe9;padding:3px 0;font-size:10.5px}.math-task:last-child{border-bottom:0}.math-task code{font:400 9px 'JetBrains Mono',monospace;color:#3b5a8f}.math-task em{grid-column:1 / -1;color:#8a887f;font-size:9.5px}.answer-line{display:block;border-bottom:1px dotted #9a988f;height:12px;margin-top:2px} .model-grid{display:grid;grid-template-columns:1.5fr 1fr;column-gap:16px}.compact-table{width:100%;border-collapse:collapse;font-size:10px;line-height:1.25}.compact-table th{background:#f4f3ef;border:1px solid #1c1c1a;padding:3px 6px;text-align:left}.compact-table td{border:1px solid #d8d6d0;padding:3px 6px;vertical-align:top}.compact-table code{font:400 9px 'JetBrains Mono',monospace}.constants td:first-child{width:68%}.check-table td:first-child,.procedure-table td:first-child,.git-gate td:first-child{width:24px;text-align:center}.check-table td:nth-child(2){width:95px} .command-list{margin:0 0 0 22px;padding:0;font-size:10.5px;line-height:1.45}.command-list code{font:400 9px 'JetBrains Mono',monospace}.safety-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}.safety-card{border:1px solid #d8d6d0;padding:5px 7px}.safety-card strong{display:block;font:600 9px 'JetBrains Mono',monospace;color:#3b5a8f;margin-bottom:2px}.safety-card span{font-size:10.5px;line-height:1.32;text-align:justify} @@ -3351,11 +3492,8 @@ def build_5b_pages_html(data: dict) -> list[dict]: "center": "".join( [ render_metric_header_5a_html(data), - render_mission_5a_html(data), render_objectives_5a_html(data), render_agenda_5a_html(data), - render_hw_gate_5a_html(data), - render_tech_stack_5a_html(data), ] ), }, @@ -3364,7 +3502,10 @@ def build_5b_pages_html(data: dict) -> list[dict]: "center": "".join( [ render_running_card_header_5a_html(data), + render_mission_5a_html(data), render_theory_5a_html(data), + render_tech_stack_5a_html(data), + render_hw_gate_5a_html(data), render_math_5a_html(data), ] ), @@ -3499,7 +3640,7 @@ body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items: .tree-gap-left,.tree-gap-right{min-width:0}.tree-we{font-weight:600;color:#3b5a8f;margin-top:2px}.tree-we:first-child{margin-top:0}.tree-line.level-1{padding-left:5px}.tree-line.level-2{padding-left:10px;color:#4a4944} .page-main{display:flex;flex-direction:column;min-height:0;justify-content:space-between} .page.page-technical{--page-rule-left:104px;--page-rule-width:915px;width:1123px;height:794px}.page-technical .technical-page-grid{grid-template-columns:77px 21px minmax(0,1fr) 21px 77px}.technical-main{display:flex;min-height:0}.technical-section{flex:1;display:flex;flex-direction:column;margin-top:0}.technical-section .section-body{flex:1;min-height:0;padding:6px 8px}.technical-sheet-figure{height:100%;min-height:0;margin:0;display:flex;flex-direction:column;gap:4px}.technical-sheet-link{flex:1;min-height:0;display:flex;align-items:center;justify-content:center}.technical-sheet-figure img{width:100%;height:100%;object-fit:contain;border:1px solid #d8d6d0;background:white}.technical-sheet-figure figcaption{display:flex;justify-content:space-between;align-items:baseline;gap:10px;font-size:7.2px;line-height:1.18;color:#4a4944}.technical-sheet-figure figcaption span{min-width:0}.technical-sheet-missing{flex:1;display:flex;align-items:center;justify-content:center;border:1px solid #d8d6d0;color:#8a887f;font:600 8px 'JetBrains Mono',monospace;text-transform:uppercase} -.metric-box{border:1.4px solid #1c1c1a}.metric-title{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:7px 10px;border-bottom:1px solid #1c1c1a}.metric-title div{font-size:12.5px;font-weight:700}.metric-title span{font:500 8px 'JetBrains Mono',monospace;color:#4a4944;white-space:nowrap} +.metric-box{border:1.4px solid #1c1c1a}.title-block{display:grid;grid-template-columns:1.0fr 2.35fr 1.24fr 52px;min-height:82px;border-bottom:1px solid #1c1c1a}.title-signers,.title-document{display:grid;grid-template-rows:repeat(3,minmax(0,1fr));min-width:0}.title-signers{border-right:1px solid #1c1c1a}.title-document{border-right:1px solid #1c1c1a}.title-meta{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:repeat(3,minmax(0,1fr));min-width:0}.title-cell,.title-name,.title-url{display:flex;flex-direction:column;justify-content:center;min-width:0;padding:2px 4px;border-bottom:1px solid #d8d6d0}.title-signers .title-cell:last-child,.title-document>div:last-child,.title-meta .title-cell:nth-last-child(-n+2){border-bottom:0}.title-meta .title-cell:nth-child(odd){border-right:1px solid #d8d6d0}.title-cell span,.title-name span,.title-url span{font:600 5.4px/1.12 'JetBrains Mono',monospace;letter-spacing:.07em;text-transform:uppercase;color:#8a887f}.title-cell strong,.title-name strong{font-size:6.9px;line-height:1.1;font-weight:500;overflow-wrap:anywhere}.title-cell em{font:400 5.2px/1.05 'JetBrains Mono',monospace;color:#8a887f;font-style:normal}.title-category{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:2px 4px;background:#3b5a8f;color:#fefefc;font-size:5.9px;letter-spacing:.04em;overflow:hidden}.title-category strong{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.title-category span{font:400 5.1px 'JetBrains Mono',monospace;white-space:nowrap;color:#fefefc}.title-name strong{font-size:7.6px;font-weight:700}.title-url code{font:400 5.4px/1.12 'JetBrains Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#4a4944}.title-qr-cell{display:flex;align-items:center;justify-content:center;padding:4px;border-left:1px solid #1c1c1a;background:#fefefc}.title-qr{width:100%;height:100%;display:block}.title-qr-empty{flex-direction:column;text-align:center;color:#8a887f}.title-qr-empty span{font:600 6px 'JetBrains Mono',monospace}.title-qr-empty strong{font:400 5px 'JetBrains Mono',monospace}.metric-title{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:7px 10px;border-bottom:1px solid #1c1c1a}.metric-title div{font-size:12.5px;font-weight:700}.metric-title span{font:500 8px 'JetBrains Mono',monospace;color:#4a4944;white-space:nowrap} .metric-grid{display:grid;grid-template-columns:1fr 1fr;font-size:9.4px}.metric-grid div{display:flex;gap:6px;padding:3px 10px;border-bottom:1px solid #d8d6d0}.metric-grid div:nth-child(odd){border-right:1px solid #d8d6d0}.metric-grid div:nth-last-child(-n+2){border-bottom:0}.metric-grid span{font:500 7.2px/1.7 'JetBrains Mono',monospace;color:#8a887f;text-transform:uppercase;flex:none}.metric-grid strong{font-weight:400}.metric-grid i{flex:1;border-bottom:1px dotted #9a988f} .plain-box{border:1px solid #1c1c1a;padding:6px 10px;margin-top:7px}.plain-title{font-weight:700;font-size:9.8px;letter-spacing:.12em;text-transform:uppercase;margin-bottom:1px}.plain-box p{margin:0;text-align:justify} .section-box{border:1px solid #1c1c1a;margin-top:7px}.section-box:first-child{margin-top:0}.section-box.dense{margin-top:6px}.section-head{display:flex;align-items:baseline;gap:6px;padding:4px 10px;background:#f4f3ef;border-bottom:1px solid #1c1c1a}.section-head span{font:600 7.8px 'JetBrains Mono',monospace;color:#8a887f}.section-head strong{font-weight:700;font-size:9.6px;letter-spacing:.12em;text-transform:uppercase}.section-body{padding:6px 10px}.dense .section-body{padding:5px 9px} @@ -3511,7 +3652,7 @@ body{margin:0;background:#eceae4;display:flex;flex-direction:column;align-items: .theory-table{width:100%;border-collapse:collapse;font-size:9.4px}.theory-table th{text-align:left;padding:2px 7px;border:1px solid #1c1c1a;background:#f4f3ef}.theory-table th:first-child{width:31%}.theory-table td{padding:2px 7px;vertical-align:top;border:1px solid #d8d6d0}.theory-table code{font:400 8px 'JetBrains Mono',monospace} .tech-stack{display:grid;grid-template-columns:1fr 1fr;column-gap:10px;align-items:start}.stack-column h3{font:600 7.2px 'JetBrains Mono',monospace;letter-spacing:.08em;color:#4a4944;text-transform:uppercase;padding-bottom:2px;border-bottom:1px solid #1c1c1a;margin:0}.stack-grid{display:grid;grid-template-columns:70px 1fr;font-size:8.6px;line-height:1.25}.stack-grid div{padding:2px 0;border-bottom:1px solid #e4e2dc}.stack-grid div:nth-last-child(-n+2){border-bottom:0}.stack-grid code{font:400 7.7px/1.35 'JetBrains Mono',monospace} .part-banner{border:1.4px solid #1c1c1a;background:#f4f3ef;padding:5px 10px;display:grid;grid-template-columns:126px 1fr;column-gap:9px;align-items:baseline}.part-banner strong{font:700 9px 'JetBrains Mono',monospace;letter-spacing:.08em}.part-banner span{font-size:9.4px;text-align:justify} -.compact-list{margin:2px 0 0 13px;padding:0;font-size:8.7px;line-height:1.25}.compact-list li{margin:1px 0}.three-col,.connection-notes{display:grid;grid-template-columns:1fr 1fr 1fr;column-gap:8px}.connection-notes{grid-template-columns:1fr 1fr;margin-top:4px}.three-col h3,.model-grid h3,.connection-notes h3{font:600 7.2px 'JetBrains Mono',monospace;text-transform:uppercase;color:#4a4944;margin:0 0 2px}.three-col p,.model-grid p{margin:0 0 3px;font-size:8.8px}.pass-line{margin:4px 0 0;padding-top:3px;border-top:1px solid #e4e2dc;font-size:8.6px} +.compact-list{margin:2px 0 0 13px;padding:0;font-size:8.7px;line-height:1.25}.compact-list li{margin:1px 0}.code-subblock{border:1px solid #d8d6d0;border-left:3px solid #3b5a8f;padding:4px 5px;margin-top:4px}.code-subblock:first-child{margin-top:0}.code-subblock h3{margin:0 0 2px}.code-subblock>p{margin:0 0 3px}.three-col,.connection-notes{display:grid;grid-template-columns:1fr 1fr 1fr;column-gap:8px}.connection-notes{grid-template-columns:1fr 1fr;margin-top:4px}.three-col h3,.model-grid h3,.connection-notes h3{font:600 7.2px 'JetBrains Mono',monospace;text-transform:uppercase;color:#4a4944;margin:0 0 2px}.three-col p,.model-grid p{margin:0 0 3px;font-size:8.8px}.pass-line{margin:4px 0 0;padding-top:3px;border-top:1px solid #e4e2dc;font-size:8.6px} .math-task{display:grid;grid-template-columns:1fr 126px;column-gap:8px;align-items:start;border-bottom:1px solid #f0efe9;padding:3px 0;font-size:9px}.math-task:last-child{border-bottom:0}.math-task code{font:400 7.5px 'JetBrains Mono',monospace;color:#3b5a8f}.math-task em{grid-column:1 / -1;color:#8a887f;font-size:7.8px}.answer-line{display:block;border-bottom:1px dotted #9a988f;height:12px;margin-top:2px} .model-grid{display:grid;grid-template-columns:1.25fr 1fr;column-gap:10px}.compact-table{width:100%;border-collapse:collapse;font-size:8.4px;line-height:1.18}.compact-table th{background:#f4f3ef;border:1px solid #1c1c1a;padding:2px 4px;text-align:left}.compact-table td{border:1px solid #d8d6d0;padding:2px 4px;vertical-align:top}.compact-table code{font:400 7.5px 'JetBrains Mono',monospace}.constants td:first-child{width:66%}.check-table td:first-child,.procedure-table td:first-child,.git-gate td:first-child{width:20px;text-align:center}.check-table td:nth-child(2){width:70px}.procedure-table td{height:25px} .command-list{margin:0 0 0 18px;padding:0;font-size:8.8px;line-height:1.32}.command-list code{font:400 7.5px 'JetBrains Mono',monospace}.safety-grid{display:grid;grid-template-columns:1fr 1fr;gap:5px}.safety-card{border:1px solid #d8d6d0;padding:4px 5px}.safety-card strong{display:block;font:600 7.5px 'JetBrains Mono',monospace;color:#3b5a8f;margin-bottom:1px}.safety-card span{font-size:8.4px;line-height:1.22;text-align:justify} @@ -3560,6 +3701,7 @@ def render_preamble_5a(card: dict) -> list[str]: r"\usepackage[polish]{babel}", r"\usepackage{array}", r"\usepackage{tabularx}", + r"\usepackage{multirow}", r"\usepackage{xcolor}", r"\usepackage{amssymb}", r"\usepackage{graphicx}", @@ -3568,6 +3710,7 @@ def render_preamble_5a(card: dict) -> list[str]: r"\IfFileExists{stix2.sty}{\usepackage{stix2}}{\usepackage{lmodern}}", r"\IfFileExists{inconsolata.sty}{\usepackage{inconsolata}}{\usepackage{lmodern}}", r"\usepackage[most]{tcolorbox}", + r"\usepackage{qrcode}", r"\usepackage{hyperref}", r"\hypersetup{hidelinks}", r"\IfFileExists{build-meta.tex}{\input{build-meta.tex}}{\newcommand{\BuildCommit}{local}}", @@ -3596,6 +3739,7 @@ def render_preamble_5a(card: dict) -> list[str]: r"\newcommand{\FiveAWriteLine}{\par\noindent\dotfill}", r"\newtcolorbox{FiveASection}[2]{enhanced,arc=0pt,boxrule=0.35pt,colback=ESCPaper,colframe=ESCInk,boxsep=0pt,left=2.8mm,right=2.8mm,top=1.2mm,bottom=1.2mm,colbacktitle=ESCHeader,coltitle=ESCInk,fonttitle=\bfseries\footnotesize,title={\ttfamily\textcolor{ESCMuted}{\S#1}\quad #2}}", r"\newtcolorbox{FiveAPart}{enhanced,arc=0pt,boxrule=0.6pt,colback=ESCHeader,colframe=ESCInk,boxsep=0pt,left=2.8mm,right=2.8mm,top=1.2mm,bottom=1.2mm}", + r"\newtcolorbox{CardContentBlock}{enhanced,arc=0pt,boxrule=0.28pt,colback=ESCPaper,colframe=ESCLine,boxsep=0pt,left=1.4mm,right=1.4mm,top=0.8mm,bottom=0.8mm}", "", ] @@ -3637,18 +3781,45 @@ def render_margin_tree_panel_5a_tex(data: dict, column: str) -> list[str]: def render_metric_header_5a_tex(data: dict) -> list[str]: card = data["card"] + title_fields = title_block_fields(data) fields = data.get("metric_fill_fields") or [{"label": "Uczeń"}, {"label": "Klasa · data"}] first_label = fields[0].get("label", "Uczeń") if fields else "Uczeń" second_label = fields[1].get("label", "Klasa · data") if len(fields) > 1 else "Klasa · data" + + def title_cell(label: str, value: object, mono: bool = False) -> str: + content = tex_escape(str(value)) if mono else tex_5a(value) + face = r"\ttfamily" if mono else "" + return rf"{{\tiny\ttfamily\textcolor{{ESCMuted}}{{{tex_escape(label.upper())}}}}}\par{{\scriptsize {face} {content}}}" + + url = str(title_fields["url"]) + safe_qr_url = re.fullmatch(r"https?://[A-Za-z0-9._~:/?&=+\-]+", url) + qr = ( + rf"\qrcode[height=15.5mm,level=L]{{{url}}}" + if title_fields["show_qr"] and safe_qr_url + else r"{\tiny\ttfamily\textcolor{ESCMuted}{QR: brak URL}}" + ) return [ - r"\begin{tcolorbox}[enhanced,arc=0pt,boxrule=0.6pt,colback=ESCPaper,colframe=ESCInk,boxsep=0pt,left=2.8mm,right=2.8mm,top=1.8mm,bottom=1.8mm]", - rf"{{\bfseries {tex_5a(card_title_line_5a(card))}}}\hfill{{\scriptsize\ttfamily KARTA {tex_escape(card['number'])}/{tex_escape(card['count'])}}}\par", - r"\vspace{0.2em}\hrule\vspace{0.2em}", - r"\scriptsize", - r"\begin{tabularx}{\linewidth}{@{}>{\ttfamily\textcolor{ESCMuted}}p{2.2cm}X>{\ttfamily\textcolor{ESCMuted}}p{2.5cm}X@{}}", - rf"Projekt & {tex_5a(card['project'])} & Przedmiot & {tex_5a(card['subject'])} \\", - rf"{tex_5a(first_label)} & \FiveAFill & {tex_5a(second_label)} & \FiveAFill \\", + r"\begin{tcolorbox}[enhanced,arc=0pt,boxrule=0.6pt,colback=ESCPaper,colframe=ESCInk,boxsep=0pt,left=0pt,right=0pt,top=0pt,bottom=0pt]", + r"\renewcommand{\arraystretch}{1.08}\scriptsize", + r"\noindent\begin{minipage}[t]{12.40cm}", + r"\begin{tabularx}{\linewidth}{|p{2.55cm}|Y|p{1.55cm}|p{1.75cm}|}", + r"\hline", + rf"{title_cell('Opracował', title_fields['prepared_by'])}\newline{{\tiny\ttfamily\textcolor{{ESCMuted}}{{{tex_escape(str(title_fields['prepared_on']))}}}}} & " + rf"{{\scriptsize\bfseries {tex_5a(title_fields['category'])}}}\hfill{{\tiny\ttfamily {tex_escape(str(title_fields['standard']))}}} & " + rf"{title_cell('Rewizja', title_fields['revision'], True)} & {title_cell('Data wydania', title_fields['issued_on'], True)} \\\hline", + rf"{title_cell('Sprawdził', title_fields['checked_by'])} & " + rf"{title_cell('Tytuł', title_fields['title'])} & " + rf"{title_cell('Seria', title_fields['series'], True)} & {title_cell('Typ', title_fields['document_type'])} \\\hline", + rf"{title_cell('Zatwierdził', title_fields['approved_by'])} & " + rf"{title_cell('URL', url or '—', True)} & " + rf"{title_cell('Narzędzie', title_fields['tool'])} & {title_cell('Arkusz', title_fields['sheet'], True)} \\\hline", r"\end{tabularx}", + r"\end{minipage}\hfill", + r"\begin{minipage}[t][15.5mm][c]{1.95cm}\centering", + qr, + r"\end{minipage}", + r"\hspace{2.0mm}{\tiny\ttfamily\textcolor{ESCMuted}{PROJEKT}}\hspace{0.8mm}{\scriptsize " + tex_5a(card["project"]) + r"}\hfill{\tiny\ttfamily\textcolor{ESCMuted}{PRZEDMIOT}}\hspace{0.8mm}{\scriptsize " + tex_5a(card["subject"]) + r"}\hspace{2.0mm}\par", + rf"\hspace{{2.0mm}}{{\tiny\ttfamily\textcolor{{ESCMuted}}{{{tex_escape(first_label.upper())}}}}}\hspace{{0.8mm}}\FiveAFill\hfill{{\tiny\ttfamily\textcolor{{ESCMuted}}{{{tex_escape(second_label.upper())}}}}}\hspace{{0.8mm}}\FiveAFill\hspace{{2.0mm}}\par", r"\end{tcolorbox}", ] @@ -3805,14 +3976,15 @@ def render_code_columns_5a_tex(block: dict) -> list[str]: def render_code_subblock_5a_tex(block: dict) -> list[str]: body = [ - rf"\par\textbf{{{tex_5a(block.get('title', ''))}}}\par", + r"\begin{CardContentBlock}", + rf"\textbf{{{tex_5a(block.get('title', ''))}}}\par", tex_5a(block.get("context", "")), r"\par\vspace{0.15em}", *render_code_columns_5a_tex(block), ] if block.get("pass_condition"): body.append(rf"\par{{\tiny\ttfamily\textcolor{{ESCAccent}}{{PASS: {tex_5a(block.get('pass_condition', ''))}}}}}") - body.append(r"\vspace{0.25em}") + body.extend([r"\end{CardContentBlock}", r"\vspace{0.25em}"]) return body @@ -4215,6 +4387,7 @@ def render_preamble_5b(card: dict) -> list[str]: r"\usepackage[polish]{babel}", r"\usepackage{array}", r"\usepackage{tabularx}", + r"\usepackage{multirow}", r"\usepackage{xcolor}", r"\usepackage{amssymb}", r"\usepackage{graphicx}", @@ -4224,6 +4397,7 @@ def render_preamble_5b(card: dict) -> list[str]: r"\IfFileExists{stix2.sty}{\usepackage{stix2}}{\usepackage{lmodern}}", r"\IfFileExists{inconsolata.sty}{\usepackage{inconsolata}}{\usepackage{lmodern}}", r"\usepackage[most]{tcolorbox}", + r"\usepackage{qrcode}", r"\usepackage{hyperref}", r"\hypersetup{hidelinks}", r"\IfFileExists{build-meta.tex}{\input{build-meta.tex}}{\newcommand{\BuildCommit}{local}}", @@ -4252,6 +4426,7 @@ def render_preamble_5b(card: dict) -> list[str]: r"\newcommand{\FiveAWriteLine}{\par\noindent\dotfill}", r"\newtcolorbox{FiveASection}[2]{enhanced,arc=0pt,boxrule=0.35pt,colback=ESCPaper,colframe=ESCInk,boxsep=0pt,left=2.0mm,right=2.0mm,top=1.0mm,bottom=1.0mm,colbacktitle=ESCHeader,coltitle=ESCInk,fonttitle=\bfseries\scriptsize,title={\ttfamily\textcolor{ESCMuted}{#1.}\quad #2}}", r"\newtcolorbox{FiveAPart}{enhanced,arc=0pt,boxrule=0.55pt,colback=ESCHeader,colframe=ESCInk,boxsep=0pt,left=2.0mm,right=2.0mm,top=1.0mm,bottom=1.0mm}", + r"\newtcolorbox{CardContentBlock}{enhanced,arc=0pt,boxrule=0.28pt,colback=ESCPaper,colframe=ESCLine,boxsep=0pt,left=1.1mm,right=1.1mm,top=0.7mm,bottom=0.7mm}", "", ] @@ -4476,15 +4651,9 @@ def build_5b_pages_tex(data: dict) -> list[list[str]]: "center": [ *render_metric_header_5a_tex(data), r"\vfill", - *render_mission_5a_tex(data), - r"\vfill", *render_objectives_5a_tex(data), r"\vfill", *render_agenda_5a_tex(data), - r"\vfill", - *render_hw_gate_5a_tex(data), - r"\vfill", - *render_tech_stack_5a_tex(data), ], }, { @@ -4492,8 +4661,14 @@ def build_5b_pages_tex(data: dict) -> list[list[str]]: "center": [ *running_card_header_5b_tex(data), r"\vfill", + *render_mission_5a_tex(data), + r"\vfill", *render_theory_5a_tex(data), r"\vfill", + *render_tech_stack_5a_tex(data), + r"\vfill", + *render_hw_gate_5a_tex(data), + r"\vfill", *render_math_5a_tex(data), ], },