""" layout/page/sections.py ------------------------ Section(分段)相關的資料邏輯與 UI 元件。 只依賴 Dash / DMC,不含任何 @callback。 公開 API: split_blocks_into_sections(blocks) → List[section_dict] get_section_blocks(sections, section_id) → List[block_dict] make_section_control(sections, active_id) → html.Div | None """ from __future__ import annotations from typing import Any from dash import html import dash_mantine_components as dmc # --------------------------------------------------------------------------- # 資料邏輯 # --------------------------------------------------------------------------- def split_blocks_into_sections(blocks: list[dict]) -> list[dict]: """ 把平坦的 block 清單按 H2 heading 切割成 section 清單。 回傳格式: [ {"id": "sec-all", "title": "全部", "blocks": [...]}, # 永遠第一筆 {"id": "sec-1", "title": "標題文字", "blocks": [...]}, ... ] """ sections: list[dict] = [ {"id": "sec-all", "title": "全部", "blocks": blocks[:]} ] current: dict | None = None sec_idx = 0 for b in blocks: if b.get("type") == "heading" and int(b.get("level", 0)) == 2: if current: sections.append(current) sec_idx += 1 current = { "id": f"sec-{sec_idx}", "title": (b.get("content") or f"段落 {sec_idx}").strip(), "blocks": [b], } else: if current is not None: current["blocks"].append(b) if current: sections.append(current) return sections def get_section_blocks( sections: list[dict], section_id: str | None ) -> list[dict]: """ 取得指定 section_id 的 block 清單。 找不到時退回第一個 section;sections 為空時回傳空清單。 """ if not sections: return [] if not section_id: return sections[0].get("blocks") or [] for s in sections: if (s.get("id") or "") == section_id: return s.get("blocks") or [] return sections[0].get("blocks") or [] # --------------------------------------------------------------------------- # UI 元件 # --------------------------------------------------------------------------- def make_section_control( sections: list[dict], active_id: str | None ) -> html.Div | None: """ 產生水平捲動的 section 切換按鈕列。 若 sections 為空,回傳 None。 """ if not sections: return None buttons: list[Any] = [] for i, s in enumerate(sections): sid = s.get("id") or f"sec-{i + 1}" title = s.get("title") or f"段落 {i + 1}" buttons.append( dmc.Button( title, id={"type": "section-btn", "sid": sid}, variant="filled" if sid == active_id else "light", size="sm", style={"flex": "0 0 auto"}, ) ) return html.Div( dmc.Group(buttons, gap="sm", wrap="nowrap"), style={ "overflowX": "auto", "overflowY": "hidden", "whiteSpace": "nowrap", "paddingBottom": "6px", }, )