streamlit-flexnav 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. streamlit_flexnav-0.1.0/LICENSE +21 -0
  2. streamlit_flexnav-0.1.0/PKG-INFO +131 -0
  3. streamlit_flexnav-0.1.0/README.md +112 -0
  4. streamlit_flexnav-0.1.0/pyproject.toml +41 -0
  5. streamlit_flexnav-0.1.0/setup.cfg +4 -0
  6. streamlit_flexnav-0.1.0/src/streamlit_flexnav/__init__.py +10 -0
  7. streamlit_flexnav-0.1.0/src/streamlit_flexnav/cli.py +2 -0
  8. streamlit_flexnav-0.1.0/src/streamlit_flexnav/configs/__init__.py +2 -0
  9. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/__init__.py +2 -0
  10. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/loader.py +106 -0
  11. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/menuregistry.py +29 -0
  12. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/models/__init__.py +2 -0
  13. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/models/accesscontrol.py +37 -0
  14. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/models/linktarget.py +7 -0
  15. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/models/menugroup.py +30 -0
  16. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/models/menustruct.py +153 -0
  17. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/models/rolemode.py +25 -0
  18. streamlit_flexnav-0.1.0/src/streamlit_flexnav/core/settings.py +64 -0
  19. streamlit_flexnav-0.1.0/src/streamlit_flexnav/images/__init__.py +2 -0
  20. streamlit_flexnav-0.1.0/src/streamlit_flexnav/logging_setup.py +59 -0
  21. streamlit_flexnav-0.1.0/src/streamlit_flexnav/menupages/__menu__.py +11 -0
  22. streamlit_flexnav-0.1.0/src/streamlit_flexnav/menupages/settings/settings_admin.py +16 -0
  23. streamlit_flexnav-0.1.0/src/streamlit_flexnav/menupages/settings/settings_user.py +16 -0
  24. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/__init__.py +2 -0
  25. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/__main__.py +164 -0
  26. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/cli.py +478 -0
  27. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/debug_paths.py +54 -0
  28. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/doctor.py +116 -0
  29. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/fix_keys.py +114 -0
  30. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/fix_streamlit_pages.py +115 -0
  31. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/linter.py +150 -0
  32. streamlit_flexnav-0.1.0/src/streamlit_flexnav/tools/startup_checks.py +46 -0
  33. streamlit_flexnav-0.1.0/src/streamlit_flexnav/ui/__init__.py +2 -0
  34. streamlit_flexnav-0.1.0/src/streamlit_flexnav/ui/converter.py +46 -0
  35. streamlit_flexnav-0.1.0/src/streamlit_flexnav/ui/navigator.py +120 -0
  36. streamlit_flexnav-0.1.0/src/streamlit_flexnav/ui/session.py +5 -0
  37. streamlit_flexnav-0.1.0/src/streamlit_flexnav.egg-info/PKG-INFO +131 -0
  38. streamlit_flexnav-0.1.0/src/streamlit_flexnav.egg-info/SOURCES.txt +40 -0
  39. streamlit_flexnav-0.1.0/src/streamlit_flexnav.egg-info/dependency_links.txt +1 -0
  40. streamlit_flexnav-0.1.0/src/streamlit_flexnav.egg-info/entry_points.txt +2 -0
  41. streamlit_flexnav-0.1.0/src/streamlit_flexnav.egg-info/requires.txt +6 -0
  42. streamlit_flexnav-0.1.0/src/streamlit_flexnav.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nednaz-IT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: streamlit-flexnav
3
+ Version: 0.1.0
4
+ Summary: Navigation toolkit, menu schema, and Streamlit integration
5
+ Author-email: Nednaz-IT <informatie@nednazit.nl>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/informatie/streamlit-flexnav
8
+ Project-URL: Source, https://github.com/informatie/streamlit-flexnav
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: python-dotenv>=1.0.0
13
+ Requires-Dist: pydantic>=2.12.5
14
+ Requires-Dist: pyyaml>=6.0.3
15
+ Requires-Dist: streamlit>=1.53.1
16
+ Requires-Dist: structlog>=25.5.0
17
+ Requires-Dist: typer>=0.21.1
18
+ Dynamic: license-file
19
+
20
+ # streamlit-flexnav
21
+
22
+ A modular, schema‑driven navigation framework for Streamlit applications.
23
+ `streamlit-flexnav` provides a clean, extensible way to define menus, pages, roles, and navigation behavior using YAML/JSON schemas — with automatic UI rendering, access control, and a powerful plugin‑ready architecture.
24
+ - [streamlit-flexnav](#streamlit-flexnav)
25
+ - [✨ Features](#-features)
26
+ - [📦 Installation](#-installation)
27
+ - [🚀 Quick Start](#-quick-start)
28
+ - [1. Create a menu schema (`menu.yaml`)](#1-create-a-menu-schema-menuyaml)
29
+ - [2. Load and register the schema](#2-load-and-register-the-schema)
30
+ - [🧭 Navigation Behavior](#-navigation-behavior)
31
+ - [⚙️ Configuration](#️-configuration)
32
+ - [🛠 CLI Tools](#-cli-tools)
33
+ - [📁 Project Structure](#-project-structure)
34
+ - [🧪 Development](#-development)
35
+ - [📄 License](#-license)
36
+ - [⭐ Acknowledgements](#-acknowledgements)
37
+ - [](#)
38
+
39
+ ---
40
+
41
+ ## ✨ Features
42
+
43
+ - **Schema‑driven navigation** (YAML/JSON)
44
+ - **Automatic Streamlit UI rendering**
45
+ - **Role‑based access control**
46
+ - **Menu groups, pages, icons, and metadata**
47
+ - **Configurable sidebar behavior**
48
+ - **CLI tools for debugging, linting, and fixing schemas**
49
+ - **Plugin‑friendly architecture**
50
+ - **Fast, reproducible builds using uv**
51
+
52
+ ---
53
+
54
+ ## 📦 Installation
55
+
56
+ Install from PyPI:
57
+ ```
58
+ pip install streamlit-flexnav
59
+ ```
60
+ Or using uv:
61
+ ```
62
+ uv add streamlit-flexnav
63
+ ```
64
+
65
+ ---
66
+ ## 🚀 Quick Start
67
+ ### 1. Create a menu schema (`menu.yaml`)
68
+ ### 2. Load and register the schema
69
+
70
+ ## 🧭 Navigation Behavior
71
+ Navigator automatically:<br>
72
+ Renders menu groups and pages<br>
73
+ Applies role‑based access control<br>
74
+ Highlights the active page<br>
75
+ Supports icons, dividers, and collapsible groups<br>
76
+ Integrates seamlessly with Streamlit’s session state
77
+
78
+ ---
79
+ ## ⚙️ Configuration
80
+
81
+ ```
82
+ TODO
83
+ menu settings
84
+ Page settings
85
+ ```
86
+ ## 🛠 CLI Tools
87
+ After installation, the CLI becomes available:<br>
88
+ streamlit-flexnav
89
+ ```
90
+ Command Description
91
+ doctor Diagnose common configuration issues
92
+ fix-keys Normalize schema keys
93
+ debug-paths Show resolved paths
94
+ linter Validate schema structure
95
+ startup-checks Run environment checks
96
+ ```
97
+
98
+ ## 📁 Project Structure
99
+ src/streamlit_flexnav/
100
+ core/ # Loaders, registries, settings, schema logic
101
+ ui/ # Streamlit UI components
102
+ tools/ # CLI tools
103
+ configs/ # Default configuration files
104
+ images/ # Icons and static assets
105
+ menupages/ # Built-in menu pages
106
+
107
+ See API_REFERENCE.md for full details.
108
+
109
+ ## 🧪 Development
110
+ Clone the repository:
111
+ ```
112
+ git clone https://github.com/informatie/streamlit-flexnav
113
+ cd streamlit-flexnav
114
+ ```
115
+ Set up the environment:
116
+ ```
117
+ uv venv
118
+ uv sync
119
+ source .venv/bin/activate
120
+ ```
121
+ ## 📄 License
122
+ MIT License — see LICENSE for details.
123
+
124
+ ## ⭐ Acknowledgements
125
+ If you want, I can also generate:
126
+
127
+ TODO
128
+
129
+ Just tell me which one you want next.
130
+
131
+ ##
@@ -0,0 +1,112 @@
1
+ # streamlit-flexnav
2
+
3
+ A modular, schema‑driven navigation framework for Streamlit applications.
4
+ `streamlit-flexnav` provides a clean, extensible way to define menus, pages, roles, and navigation behavior using YAML/JSON schemas — with automatic UI rendering, access control, and a powerful plugin‑ready architecture.
5
+ - [streamlit-flexnav](#streamlit-flexnav)
6
+ - [✨ Features](#-features)
7
+ - [📦 Installation](#-installation)
8
+ - [🚀 Quick Start](#-quick-start)
9
+ - [1. Create a menu schema (`menu.yaml`)](#1-create-a-menu-schema-menuyaml)
10
+ - [2. Load and register the schema](#2-load-and-register-the-schema)
11
+ - [🧭 Navigation Behavior](#-navigation-behavior)
12
+ - [⚙️ Configuration](#️-configuration)
13
+ - [🛠 CLI Tools](#-cli-tools)
14
+ - [📁 Project Structure](#-project-structure)
15
+ - [🧪 Development](#-development)
16
+ - [📄 License](#-license)
17
+ - [⭐ Acknowledgements](#-acknowledgements)
18
+ - [](#)
19
+
20
+ ---
21
+
22
+ ## ✨ Features
23
+
24
+ - **Schema‑driven navigation** (YAML/JSON)
25
+ - **Automatic Streamlit UI rendering**
26
+ - **Role‑based access control**
27
+ - **Menu groups, pages, icons, and metadata**
28
+ - **Configurable sidebar behavior**
29
+ - **CLI tools for debugging, linting, and fixing schemas**
30
+ - **Plugin‑friendly architecture**
31
+ - **Fast, reproducible builds using uv**
32
+
33
+ ---
34
+
35
+ ## 📦 Installation
36
+
37
+ Install from PyPI:
38
+ ```
39
+ pip install streamlit-flexnav
40
+ ```
41
+ Or using uv:
42
+ ```
43
+ uv add streamlit-flexnav
44
+ ```
45
+
46
+ ---
47
+ ## 🚀 Quick Start
48
+ ### 1. Create a menu schema (`menu.yaml`)
49
+ ### 2. Load and register the schema
50
+
51
+ ## 🧭 Navigation Behavior
52
+ Navigator automatically:<br>
53
+ Renders menu groups and pages<br>
54
+ Applies role‑based access control<br>
55
+ Highlights the active page<br>
56
+ Supports icons, dividers, and collapsible groups<br>
57
+ Integrates seamlessly with Streamlit’s session state
58
+
59
+ ---
60
+ ## ⚙️ Configuration
61
+
62
+ ```
63
+ TODO
64
+ menu settings
65
+ Page settings
66
+ ```
67
+ ## 🛠 CLI Tools
68
+ After installation, the CLI becomes available:<br>
69
+ streamlit-flexnav
70
+ ```
71
+ Command Description
72
+ doctor Diagnose common configuration issues
73
+ fix-keys Normalize schema keys
74
+ debug-paths Show resolved paths
75
+ linter Validate schema structure
76
+ startup-checks Run environment checks
77
+ ```
78
+
79
+ ## 📁 Project Structure
80
+ src/streamlit_flexnav/
81
+ core/ # Loaders, registries, settings, schema logic
82
+ ui/ # Streamlit UI components
83
+ tools/ # CLI tools
84
+ configs/ # Default configuration files
85
+ images/ # Icons and static assets
86
+ menupages/ # Built-in menu pages
87
+
88
+ See API_REFERENCE.md for full details.
89
+
90
+ ## 🧪 Development
91
+ Clone the repository:
92
+ ```
93
+ git clone https://github.com/informatie/streamlit-flexnav
94
+ cd streamlit-flexnav
95
+ ```
96
+ Set up the environment:
97
+ ```
98
+ uv venv
99
+ uv sync
100
+ source .venv/bin/activate
101
+ ```
102
+ ## 📄 License
103
+ MIT License — see LICENSE for details.
104
+
105
+ ## ⭐ Acknowledgements
106
+ If you want, I can also generate:
107
+
108
+ TODO
109
+
110
+ Just tell me which one you want next.
111
+
112
+ ##
@@ -0,0 +1,41 @@
1
+ # --------------------------------------------------------------------
2
+ # Patch: bugfix → 0.1.1
3
+ # Minor: new features → 0.2.0
4
+ # Major: breaking changes → 1.0.0
5
+ # --------------------------------------------------------------------
6
+ [project]
7
+ name = "streamlit-flexnav"
8
+ version = "0.1.0"
9
+ description = "Navigation toolkit, menu schema, and Streamlit integration"
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ license = { text = "MIT" }
13
+
14
+ authors = [
15
+ { name = "Nednaz-IT", email = "informatie@nednazit.nl" }
16
+ ]
17
+
18
+ dependencies = [
19
+ "python-dotenv>=1.0.0",
20
+ "pydantic>=2.12.5",
21
+ "pyyaml>=6.0.3",
22
+ "streamlit>=1.53.1",
23
+ "structlog>=25.5.0",
24
+ "typer>=0.21.1",
25
+ ]
26
+
27
+ [project.scripts]
28
+ streamlit-flexnav = "streamlit_flexnav.tools.cli:main"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/informatie/streamlit-flexnav"
32
+ Source = "https://github.com/informatie/streamlit-flexnav"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["streamlit_flexnav"]
36
+ sources = ["src"]
37
+
38
+ [tool.hatch.build]
39
+ include = [
40
+ "src/streamlit_flexnav/**/*",
41
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """
2
+ streamlit-flexnav
3
+ -----------------
4
+ A navigation toolkit, menu schema, and Streamlit integration layer.
5
+ """
6
+ from .ui.navigator import render_navigation
7
+
8
+ __all__ = [
9
+ "render_navigation",
10
+ ]
@@ -0,0 +1,2 @@
1
+ def main():
2
+ print("FlexNav CLI is running")
@@ -0,0 +1,2 @@
1
+ # flexnav/configs/__init__.py
2
+ # Generated: 2026-01-31 • Version 0.1.0
@@ -0,0 +1,2 @@
1
+ # flexnav/core/__init__.py
2
+ # Generated: 2026-01-31 • Version 0.1.0
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import sys
5
+ from pathlib import Path
6
+ import structlog
7
+
8
+ from streamlit_flexnav.core.settings import MENUPAGES_ROOT
9
+ from streamlit_flexnav.core.models.menustruct import MenuStruct
10
+ from streamlit_flexnav.core.models.menugroup import MenuGroup
11
+
12
+ log = structlog.get_logger().bind(component="loader")
13
+
14
+
15
+ def _load_module_from_path(path: Path):
16
+ spec = importlib.util.spec_from_file_location(path.stem, path)
17
+ module = importlib.util.module_from_spec(spec)
18
+ sys.modules[path.stem] = module
19
+ spec.loader.exec_module(module)
20
+ return module
21
+
22
+
23
+ def load_menupages() -> list[MenuStruct]:
24
+ """
25
+ Scan MENUPAGES_ROOT for .py files and extract PAGE = MenuStruct(...) definitions.
26
+ Folder structure is flattened into a single group name:
27
+ folder1/folder2/folder3 → "folder1_folder2_folder3"
28
+ """
29
+ results: list[MenuStruct] = []
30
+
31
+ for file in MENUPAGES_ROOT.rglob("*.py"):
32
+ if file.name.startswith("_"):
33
+ continue
34
+
35
+ try:
36
+ module = _load_module_from_path(file)
37
+
38
+ if not hasattr(module, "PAGE"):
39
+ log.warning("missing_PAGE_struct", file=str(file))
40
+ continue
41
+
42
+ page_struct = module.PAGE
43
+
44
+ if not isinstance(page_struct, MenuStruct):
45
+ log.error("invalid_PAGE_type", file=str(file))
46
+ continue
47
+
48
+ # Inject absolute path
49
+ page_struct.path = str(file.resolve())
50
+
51
+ # Compute flattened group name
52
+ relative = file.relative_to(MENUPAGES_ROOT)
53
+ parts = relative.parts[:-1] # folders only
54
+
55
+ group = "_".join(parts) if parts else None
56
+ page_struct.group = group
57
+
58
+ results.append(page_struct)
59
+
60
+ except Exception as e:
61
+ log.exception("failed_loading_page", file=str(file), error=str(e))
62
+
63
+ log.info("menupages_loaded", count=len(results))
64
+ return results
65
+
66
+
67
+
68
+ def load_menugroups() -> list[MenuGroup]:
69
+ """
70
+ Discover all MenuGroup definitions under MENUPAGES_ROOT.
71
+
72
+ Expected pattern inside each module:
73
+ MENU = MenuGroup(...)
74
+ """
75
+ results: list[MenuGroup] = []
76
+
77
+ for file in MENUPAGES_ROOT.rglob("*.py"):
78
+ if not file.name.startswith("__menu__"):
79
+ continue
80
+
81
+ try:
82
+ module = _load_module_from_path(file)
83
+
84
+ menu_struct = getattr(module, "MENU", None)
85
+ if menu_struct is None:
86
+ continue
87
+
88
+ if not isinstance(menu_struct, MenuGroup):
89
+ log.error("invalid_MENU_type", file=str(file))
90
+ continue
91
+
92
+ # Inject provenance
93
+ menu_struct.path = str(file.resolve())
94
+
95
+ results.append(menu_struct)
96
+
97
+ except Exception as e:
98
+ log.exception(
99
+ "failed_loading_menugroup",
100
+ file=str(file),
101
+ error=str(e),
102
+ )
103
+
104
+ log.info("menugroups_loaded", count=len(results))
105
+ return results
106
+
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import structlog
4
+ from streamlit_flexnav.core.loader import load_menupages
5
+ from streamlit_flexnav.ui.converter import MenuStruct
6
+
7
+ log = structlog.get_logger().bind(component="menuregistry")
8
+
9
+
10
+ class MenuRegistry:
11
+ """
12
+ Holds the loaded MenuStruct objects.
13
+ Reloadable, but not manually mutable.
14
+ """
15
+
16
+ def __init__(self):
17
+ self._items: list[MenuStruct] = []
18
+ self.reload()
19
+
20
+ def reload(self):
21
+ self._items = load_menupages()
22
+ log.info("menuregistry_reloaded", count=len(self._items))
23
+
24
+ def all(self) -> list[MenuStruct]:
25
+ return self._items
26
+
27
+
28
+ # Singleton
29
+ menuregistry = MenuRegistry()
@@ -0,0 +1,2 @@
1
+ # flexnav/core/__init__.py
2
+ # Generated: 2026-01-31 • Version 0.1.0
@@ -0,0 +1,37 @@
1
+ from typing import List
2
+ from pydantic import BaseModel, Field, model_validator
3
+ from .rolemode import RoleMode
4
+
5
+ class AccessControl(BaseModel):
6
+ """
7
+ Role-based access control for a menu item.
8
+ Ensures no overlap between allowed and denied roles.
9
+ """
10
+
11
+ allowed_roles: List[RoleMode] = Field(
12
+ default_factory=list,
13
+ description="Roles that may see/use this menu item."
14
+ )
15
+ denied_roles: List[RoleMode] = Field(
16
+ default_factory=list,
17
+ description="Roles that must never see/use this menu item."
18
+ )
19
+
20
+ @model_validator(mode="after")
21
+ def validate_no_overlap(self):
22
+ overlap = set(self.allowed_roles) & set(self.denied_roles)
23
+ if overlap:
24
+ names = ", ".join(r.display_name() for r in overlap)
25
+ raise ValueError(f"Role(s) cannot be both allowed and denied: {names}")
26
+ return self
27
+
28
+ def is_allowed(self, role: RoleMode) -> bool:
29
+ """
30
+ Returns True if the given role is permitted.
31
+ Denied roles always override allowed roles.
32
+ """
33
+ if role in self.denied_roles:
34
+ return False
35
+ if self.allowed_roles and role not in self.allowed_roles:
36
+ return False
37
+ return True
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+ class LinkTarget(str, Enum):
4
+ self_ = "_self"
5
+ blank = "_blank"
6
+ parent = "_parent"
7
+ top = "_top"
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel, Field
3
+ from typing import Optional, List
4
+
5
+
6
+ class MenuGroup(BaseModel):
7
+ """
8
+ Represents a styled menu/submenu section.
9
+ This is NOT a page. It is a visual grouping.
10
+ """
11
+
12
+ key: str = Field(..., description="Unique group identifier")
13
+ label: str = Field(..., description="Displayed group label")
14
+
15
+ order: int = Field(
16
+ default=100,
17
+ ge=-10_000,
18
+ le=10_000,
19
+ description="Ordering in of menu",
20
+ )
21
+
22
+ # Styling
23
+ icon: Optional[str] = None
24
+ color: Optional[str] = None
25
+ bold: bool = False
26
+ italic: bool = False
27
+ size: Optional[int] = None # 1–3 simulated sizes
28
+
29
+ # Pages inside this group
30
+ pages: List[object] = Field(default_factory=list)