""" layout/page/page_layout.py --------------------------- 頁面主體 layout 組裝。 只負責把各子模組的產出組合成最終的 dmc.Container, 不含資料邏輯,也不直接定義 callback。 公開 API: make_test_page(json_source, *, top_offset, show_src) → dmc.Container """ from __future__ import annotations from typing import Any import traceback from dash import html, dcc import dash_mantine_components as dmc from ..renderer.renderer import render_doc try: from ..renderer.renderer import install_callbacks_once except Exception: install_callbacks_once = None from ..toc import make_toc from .loader import load_json, build_toc_titles from .sections import ( split_blocks_into_sections, get_section_blocks, make_section_control, ) from .page_callbacks import register_page_callbacks # --------------------------------------------------------------------------- # 回到頂端按鈕樣式(獨立常數,方便日後調整) # --------------------------------------------------------------------------- _BACK_TO_TOP_STYLE: dict[str, Any] = { "position": "fixed", "right": "24px", "bottom": "24px", "width": "46px", "height": "46px", "borderRadius": "999px", "border": "none", "textDecoration": "none", "display": "flex", "alignItems": "center", "justifyContent": "center", "background": "rgba(0, 0, 0, 0.35)", "color": "white", "fontSize": "22px", "cursor": "pointer", "zIndex": 999, "backdropFilter": "blur(4px)", "boxShadow": "0 4px 12px rgba(0,0,0,0.18)", } # TOC 欄固定定位樣式 def _toc_fixed_style(top_offset: int) -> dict[str, Any]: return { "position": "sticky", "top": f"{top_offset + 12}px", "maxHeight": f"calc(100vh - {top_offset + 32}px)", "overflowY": "auto", "overflowX": "hidden", "paddingRight": "4px", "alignSelf": "flex-start", } # --------------------------------------------------------------------------- # 主要組裝函式 # --------------------------------------------------------------------------- def make_test_page( json_source: Any, *, top_offset: int = 90, show_src: bool = True, ) -> dmc.Container: """ 組裝完整的頁面 Container。 json_source:可傳入檔案路徑(str/Path)、已解析的 dict 或 list。 top_offset: 頁首高度(px),用於 TOC 固定定位與 scroll offset。 show_src: 保留參數(尚未使用),預留供未來顯示原始 JSON 用。 """ # 1. 確保 dashTest callbacks 已掛載 if callable(install_callbacks_once): try: install_callbacks_once() except Exception: pass # 2. 確保頁面 callbacks 已掛載 register_page_callbacks() # 3. 載入資料 data = load_json(json_source) blocks = data.get("blocks", []) sections = data.get("sections", []) or [] raw_toc = data.get("toc", []) or [] # 4. 若 JSON 未提供 sections,自動按 H2 切割 if not sections: sections = split_blocks_into_sections(blocks) # 5. 決定初始 section default_section_id = sections[0].get("id") if sections else None current_blocks = get_section_blocks(sections, default_section_id) if sections else blocks # 6. 渲染初始內容 try: content = render_doc(current_blocks) except Exception as e: content = dmc.Alert( f"render_doc 發生錯誤:{e}\n\n{traceback.format_exc(limit=2)}", title="渲染失敗", color="red", variant="filled", ) # 7. Section 控制列 section_control = make_section_control(sections, default_section_id) # 8. 左欄:Store + 控制列 + 內容 left_children: list[Any] = [ dcc.Store(id="page-sections-store", data=sections), dcc.Store(id="page-raw-toc-store", data=raw_toc), dcc.Store(id="page-active-section-store", data=default_section_id), ] if section_control is not None: left_children.append(section_control) left_children.append(html.Div(content, id="section-content-wrapper")) left_area = dmc.Stack(left_children, gap="md", style={"minWidth": 0}) # 9. TOC(僅當有標題時才產生) toc_titles = build_toc_titles(current_blocks, raw_toc) has_toc = bool(toc_titles) toc_component = ( make_toc( titles=toc_titles, top_offset=top_offset, col_span={"xs": 0, "sm": 0, "md": 3, "lg": 3, "xl": 3}, visible_from="md", ) if has_toc else None ) # 10. Grid 欄位配置 main_span = {"base": 12, "md": 9 if has_toc else 12} toc_span = {"base": 12, "md": 3} if has_toc else {"base": 12, "md": 0} toc_style = _toc_fixed_style(top_offset) if has_toc else {} cols = [ dmc.GridCol(left_area, id="main-content-col", span=main_span), dmc.GridCol( html.Div( id="section-toc-wrapper", children=toc_component, style=toc_style, ), id="toc-col", span=toc_span, className="toc-col-wrapper", style={"display": "block" if has_toc else "none"}, ), ] # compat:空 Grid 讓 Dash 的 pattern-matching callback 不出錯 grid = dmc.Grid(cols, gutter="xl") compat = dmc.Grid(id="Grid", style={"display": "none"}) return dmc.Container( [ html.Div(id="page-top-anchor"), dmc.Stack([grid, compat], gap="md"), html.A( "↑", href="#page-top-anchor", title="回到最上方", id="back-to-top-btn", style=_BACK_TO_TOP_STYLE, ), ], fluid=True, style={"overflow": "visible"}, )