hedron-sim 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.
@@ -0,0 +1,35 @@
1
+ .DS_Store
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ .eggs/
7
+ dist/
8
+ /build/
9
+ # Artifact/output build trees only — never ignore package source such as
10
+ # packages/hedron/src/hedron/build (Hatchling respects gitignore and would
11
+ # omit it from sdists/wheels; see #32).
12
+ **/.hedron/build/
13
+ packages/hedron-runtime-java/build/
14
+ target/
15
+ .venv*/
16
+ venv*/
17
+ .env
18
+ .coverage
19
+ htmlcov/
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+ .ruff_cache/
23
+ .hypothesis/
24
+ .tox/
25
+ .nox/
26
+ .audit-build-proj/
27
+ examples/reference-app/.hedron/
28
+ *.html.gz
29
+ /site/
30
+ *.so
31
+ *.dylib
32
+ *.pyd
33
+ .devcontainer/
34
+ # Local SQLite demos (e.g. examples/notes-sqlalchemy)
35
+ *.db
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Fixed
6
+
7
+ - Store route tables in ``<template data-hedron-sim-routes>`` instead of
8
+ ``<script type="application/json">``. MkDocs Material ``navigation.instant``
9
+ strips ``<script>`` nodes from fetched pages, which left demo buttons dead until
10
+ a hard refresh.
11
+ - Escape ``__HEDRON_SIM_FORM:*__`` token substitutions with ``escapeHtml`` so invite
12
+ demos cannot inject markup via form fields.
13
+ - Boot on ``DOMContentLoaded`` even when Material's ``document$`` is present, and
14
+ handle Text-node click targets / form-inherited ``hx-*`` on submit buttons.
15
+ - Expand docs sim includes *after* Markdown so `__HEDRON_SIM_UTC__` tokens are not
16
+ turned into `<strong>` (which broke timestamp swaps on Read the Docs).
17
+ - Neutralize *all* progressive `a[href]` / `form[action]` targets inside sim islands
18
+ (not only `hx-*` anchors), and block leftover navigations/submits in capture phase
19
+ so demo clicks cannot hit Read the Docs / Cloudflare WAF paths.
20
+ - Force `action="#"` on every sim `<form>` (even when `action` was omitted) and always
21
+ `preventDefault` sim submits before route init, so `method="post"` forms cannot POST
22
+ the current docs URL.
23
+ - Re-check boot invariants (no root/http `href`, forms `action="#"`) and record repairs
24
+ on `data-hedron-sim-blocked` for tests.
25
+ - While a sim click/submit is in flight, reject `fetch` / `XMLHttpRequest` so a
26
+ regression cannot hit Read the Docs network paths (MkDocs/RTD traffic outside that
27
+ window is unaffected).
28
+ - Accept legacy markdown-mangled `<strong>HEDRON_SIM_UTC</strong>` tokens in the JS shim.
29
+ - Intercept demo `hx-*` clicks/submits in the capture phase so MkDocs Material
30
+ instant navigation cannot follow progressive-enhancement `href`s out of the docs.
31
+ - Rewrite demo anchor `href`s to `#` at boot (original kept in `data-hedron-sim-href`)
32
+ because Material registers its capture listener before extra scripts.
33
+
34
+ ### Added
35
+
36
+ - Route ``validate="credentials"`` + ``variants`` for docs auth demos (``ada`` /
37
+ ``correct-horse``).
38
+ - Route extras ``accumulate="field"`` + ``empty=...`` and ``list_remove=True`` so list
39
+ demos (CRUD notes) append and delete items client-side instead of replacing one row.
40
+ - Layout styles for ``OobHost`` rows so the host id and caption are not jammed together.
41
+ - Docs theme tokens for Material `slate` / `default` schemes.
42
+ - Bounded `hx-trigger="load"` and `hx-confirm` support in the JS shim.
43
+ - PAGE / FRAGMENT mode-toggle helper for core-concepts demos.
44
+
45
+ ## [0.1.0] — 2026-08-07
46
+
47
+ ### Added
48
+
49
+ - Initial Alpha: `SimApp`, `embed_demo`, `sim_utc` / `sim_form` placeholders, and a browser HTMX shim.
50
+ - Route extras: email `validate` + `variants`, and `sequence` responses for poll-style demos.
51
+ - Docs guides and HTMX-native component galleries migrated onto generated `<!-- hedron-sim:… -->` islands.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Odos Matthews
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,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: hedron-sim
3
+ Version: 0.1.0
4
+ Summary: Offline HTMX simulation for Hedron docs and demos — real components, no live server
5
+ Project-URL: Homepage, https://github.com/eddiethedean/hedron
6
+ Project-URL: Repository, https://github.com/eddiethedean/hedron
7
+ Project-URL: Issues, https://github.com/eddiethedean/hedron/issues
8
+ Project-URL: Changelog, https://github.com/eddiethedean/hedron/blob/main/packages/hedron-sim/CHANGELOG.md
9
+ Project-URL: Documentation, https://hedron.readthedocs.io/en/latest/
10
+ Author-email: Odos Matthews <odosmatthews@gmail.com>
11
+ Maintainer-email: Odos Matthews <odosmatthews@gmail.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: components,docs,hedron,htmx,simulation
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: <3.15,>=3.11
27
+ Requires-Dist: hedron-core<0.23,>=0.22.0
28
+ Requires-Dist: hedron<0.23,>=0.22.0
29
+ Description-Content-Type: text/markdown
30
+
31
+ # hedron-sim
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/hedron-sim.svg)](https://pypi.org/project/hedron-sim/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/hedron-sim.svg)](https://pypi.org/project/hedron-sim/)
35
+ [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/hedron/ci.yml?branch=main&label=CI)](https://github.com/eddiethedean/hedron/actions/workflows/ci.yml)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/eddiethedean/hedron/blob/main/LICENSE)
37
+
38
+ Offline HTMX simulation for Hedron docs and demos.
39
+
40
+ Author demos with ordinary Hedron components (`Page`, `RefreshButton`, `swap`,
41
+ regions), then embed them in static docs. A small JavaScript runtime intercepts
42
+ `hx-*` attributes and serves pre-rendered fragment HTML — no FastAPI process
43
+ required.
44
+
45
+ **Package maturity:** Alpha (`0.1.x`) · pin `>=0.1.0,<0.2` and expect churn
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install "hedron-sim>=0.1.0,<0.2"
51
+ # or
52
+ uv add "hedron-sim>=0.1.0,<0.2"
53
+ ```
54
+
55
+ Requires Python 3.11–3.14 and `hedron` (for component imports used in demos).
56
+
57
+ ## Quick start
58
+
59
+ ```python
60
+ from hedron import Page, RefreshButton, Stack, Text, html, swap
61
+ from hedron_sim import SimApp, embed_demo, sim_utc
62
+
63
+ app = SimApp(demo_id="hello-status")
64
+ status = app.region("service-status")
65
+
66
+
67
+ def status_panel():
68
+ return html.div(
69
+ Text(f"All systems operational · refreshed {sim_utc()}"),
70
+ id=status.id,
71
+ role="status",
72
+ aria={"live": "polite"},
73
+ )
74
+
75
+
76
+ @app.page("/")
77
+ def home() -> Page:
78
+ return Page(
79
+ Stack(
80
+ Text("Hello from hedron-sim"),
81
+ status_panel(),
82
+ RefreshButton.for_region(status, href="/status", label="Refresh status"),
83
+ ),
84
+ title="Demo",
85
+ )
86
+
87
+
88
+ @app.fragment("/status", region=status)
89
+ def refresh_status():
90
+ return swap(status_panel())
91
+
92
+
93
+ print(embed_demo(app))
94
+ ```
95
+
96
+ ### Ship static assets
97
+
98
+ ```python
99
+ from pathlib import Path
100
+ from hedron_sim.assets import copy_assets
101
+
102
+ copy_assets(Path("docs/javascript"), Path("docs/stylesheets"))
103
+ ```
104
+
105
+ Then load `hedron-sim.js` (and optionally `hedron-sim.css`) from MkDocs
106
+ `extra_javascript` / `extra_css`.
107
+
108
+ ## Public API
109
+
110
+ | Symbol | Role |
111
+ |---|---|
112
+ | `SimApp` | Offline app with `@page` / `@fragment` and regions |
113
+ | `embed_demo(app)` | HTML snippet for static docs |
114
+ | `sim_utc` / `sim_local_time` | Deterministic clock helpers for demos |
115
+ | `hedron_sim.assets.copy_assets` | Copy JS/CSS into a docs tree |
116
+
117
+ ## Links
118
+
119
+ - [Package docs](https://hedron.readthedocs.io/en/latest/packages/hedron-sim/)
120
+ - [Documentation](https://hedron.readthedocs.io/en/latest/)
121
+ - [Changelog](https://github.com/eddiethedean/hedron/blob/main/packages/hedron-sim/CHANGELOG.md)
122
+ - [Source](https://github.com/eddiethedean/hedron/tree/main/packages/hedron-sim)
123
+ - [Issues](https://github.com/eddiethedean/hedron/issues)
124
+ - [`hedron`](https://pypi.org/project/hedron/)
125
+
126
+ ## License
127
+
128
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,98 @@
1
+ # hedron-sim
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/hedron-sim.svg)](https://pypi.org/project/hedron-sim/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/hedron-sim.svg)](https://pypi.org/project/hedron-sim/)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/eddiethedean/hedron/ci.yml?branch=main&label=CI)](https://github.com/eddiethedean/hedron/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/eddiethedean/hedron/blob/main/LICENSE)
7
+
8
+ Offline HTMX simulation for Hedron docs and demos.
9
+
10
+ Author demos with ordinary Hedron components (`Page`, `RefreshButton`, `swap`,
11
+ regions), then embed them in static docs. A small JavaScript runtime intercepts
12
+ `hx-*` attributes and serves pre-rendered fragment HTML — no FastAPI process
13
+ required.
14
+
15
+ **Package maturity:** Alpha (`0.1.x`) · pin `>=0.1.0,<0.2` and expect churn
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install "hedron-sim>=0.1.0,<0.2"
21
+ # or
22
+ uv add "hedron-sim>=0.1.0,<0.2"
23
+ ```
24
+
25
+ Requires Python 3.11–3.14 and `hedron` (for component imports used in demos).
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ from hedron import Page, RefreshButton, Stack, Text, html, swap
31
+ from hedron_sim import SimApp, embed_demo, sim_utc
32
+
33
+ app = SimApp(demo_id="hello-status")
34
+ status = app.region("service-status")
35
+
36
+
37
+ def status_panel():
38
+ return html.div(
39
+ Text(f"All systems operational · refreshed {sim_utc()}"),
40
+ id=status.id,
41
+ role="status",
42
+ aria={"live": "polite"},
43
+ )
44
+
45
+
46
+ @app.page("/")
47
+ def home() -> Page:
48
+ return Page(
49
+ Stack(
50
+ Text("Hello from hedron-sim"),
51
+ status_panel(),
52
+ RefreshButton.for_region(status, href="/status", label="Refresh status"),
53
+ ),
54
+ title="Demo",
55
+ )
56
+
57
+
58
+ @app.fragment("/status", region=status)
59
+ def refresh_status():
60
+ return swap(status_panel())
61
+
62
+
63
+ print(embed_demo(app))
64
+ ```
65
+
66
+ ### Ship static assets
67
+
68
+ ```python
69
+ from pathlib import Path
70
+ from hedron_sim.assets import copy_assets
71
+
72
+ copy_assets(Path("docs/javascript"), Path("docs/stylesheets"))
73
+ ```
74
+
75
+ Then load `hedron-sim.js` (and optionally `hedron-sim.css`) from MkDocs
76
+ `extra_javascript` / `extra_css`.
77
+
78
+ ## Public API
79
+
80
+ | Symbol | Role |
81
+ |---|---|
82
+ | `SimApp` | Offline app with `@page` / `@fragment` and regions |
83
+ | `embed_demo(app)` | HTML snippet for static docs |
84
+ | `sim_utc` / `sim_local_time` | Deterministic clock helpers for demos |
85
+ | `hedron_sim.assets.copy_assets` | Copy JS/CSS into a docs tree |
86
+
87
+ ## Links
88
+
89
+ - [Package docs](https://hedron.readthedocs.io/en/latest/packages/hedron-sim/)
90
+ - [Documentation](https://hedron.readthedocs.io/en/latest/)
91
+ - [Changelog](https://github.com/eddiethedean/hedron/blob/main/packages/hedron-sim/CHANGELOG.md)
92
+ - [Source](https://github.com/eddiethedean/hedron/tree/main/packages/hedron-sim)
93
+ - [Issues](https://github.com/eddiethedean/hedron/issues)
94
+ - [`hedron`](https://pypi.org/project/hedron/)
95
+
96
+ ## License
97
+
98
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "hedron-sim"
3
+ version = "0.1.0"
4
+ description = "Offline HTMX simulation for Hedron docs and demos — real components, no live server"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11,<3.15"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [
10
+ { name = "Odos Matthews", email = "odosmatthews@gmail.com" },
11
+ ]
12
+ maintainers = [
13
+ { name = "Odos Matthews", email = "odosmatthews@gmail.com" },
14
+ ]
15
+ keywords = [
16
+ "components",
17
+ "docs",
18
+ "hedron",
19
+ "htmx",
20
+ "simulation",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
33
+ "Typing :: Typed",
34
+ ]
35
+ dependencies = [
36
+ "hedron-core>=0.22.0,<0.23",
37
+ "hedron>=0.22.0,<0.23",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/eddiethedean/hedron"
42
+ Repository = "https://github.com/eddiethedean/hedron"
43
+ Issues = "https://github.com/eddiethedean/hedron/issues"
44
+ Changelog = "https://github.com/eddiethedean/hedron/blob/main/packages/hedron-sim/CHANGELOG.md"
45
+ Documentation = "https://hedron.readthedocs.io/en/latest/"
46
+
47
+ [build-system]
48
+ requires = ["hatchling"]
49
+ build-backend = "hatchling.build"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/hedron_sim"]
53
+
54
+ [tool.hatch.build.targets.wheel.force-include]
55
+ "src/hedron_sim/static/hedron-sim.js" = "hedron_sim/static/hedron-sim.js"
56
+ "src/hedron_sim/static/hedron-sim.css" = "hedron_sim/static/hedron-sim.css"
57
+
58
+ [tool.hatch.build.targets.sdist]
59
+ only-include = [
60
+ "src/hedron_sim",
61
+ "README.md",
62
+ "CHANGELOG.md",
63
+ "LICENSE",
64
+ "pyproject.toml",
65
+ ]
@@ -0,0 +1,22 @@
1
+ """Offline HTMX simulation for Hedron docs and static demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hedron_sim.app import SimApp, SimRoute
6
+ from hedron_sim.embed import embed_demo, render_handler_html, wrap_browser_chrome
7
+ from hedron_sim.tokens import SIM_LOCAL_TIME, SIM_UTC, sim_form, sim_local_time, sim_utc
8
+
9
+ __all__ = [
10
+ "SIM_LOCAL_TIME",
11
+ "SIM_UTC",
12
+ "SimApp",
13
+ "SimRoute",
14
+ "embed_demo",
15
+ "render_handler_html",
16
+ "sim_form",
17
+ "sim_local_time",
18
+ "sim_utc",
19
+ "wrap_browser_chrome",
20
+ ]
21
+
22
+ __version__ = "0.1.0"
@@ -0,0 +1,190 @@
1
+ """SimApp: register pages and fragments with ordinary Hedron handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping, Sequence
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, TypeVar
8
+
9
+ from hedron_core.interaction import FragmentRegion
10
+
11
+ __all__ = ["SimApp", "SimRoute"]
12
+
13
+ F = TypeVar("F", bound=Callable[..., Any])
14
+ Handler = Callable[..., Any]
15
+
16
+
17
+ def _as_regions(
18
+ region: FragmentRegion | str | None,
19
+ regions: Sequence[FragmentRegion | str] | None,
20
+ fragment_regions: Sequence[FragmentRegion | str] | None,
21
+ ) -> tuple[FragmentRegion, ...]:
22
+ merged: list[FragmentRegion] = []
23
+ for item in (
24
+ *(() if region is None else (region,)),
25
+ *(regions or ()),
26
+ *(fragment_regions or ()),
27
+ ):
28
+ if isinstance(item, FragmentRegion):
29
+ merged.append(item)
30
+ else:
31
+ text = str(item)
32
+ selector = text if text.startswith("#") or text.startswith(".") else f"#{text}"
33
+ region_id = text[1:] if text.startswith("#") else text
34
+ merged.append(FragmentRegion(id=region_id, selector=selector))
35
+ return tuple(merged)
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class SimRoute:
40
+ """One simulated HTTP fragment/action endpoint."""
41
+
42
+ method: str
43
+ path: str
44
+ handler: Handler
45
+ regions: tuple[FragmentRegion, ...] = ()
46
+ explanation: str = ""
47
+ validate: str | None = None
48
+ variants: Mapping[str, Handler] | None = None
49
+ sequence: tuple[Handler, ...] | None = None
50
+ accumulate: str | None = None
51
+ """Form field name — append values into a client-side list for this region."""
52
+ empty: Handler | None = None
53
+ """Handler that renders the empty list container (required with ``accumulate``)."""
54
+ list_remove: bool = False
55
+ """DELETE (or similar): remove one list item by ``data-hedron-sim-list-index``."""
56
+
57
+ @property
58
+ def key(self) -> str:
59
+ return f"{self.method.upper()} {self.path}"
60
+
61
+
62
+ @dataclass
63
+ class SimApp:
64
+ """Register a page and fragment routes for offline HTMX simulation.
65
+
66
+ Handlers use the same return types as a real Hedron app (``Page``, components,
67
+ ``InteractionResult`` / ``swap(...)``). ``embed_demo`` pre-renders them into an
68
+ HTML island plus a JSON route table consumed by ``hedron-sim.js``.
69
+
70
+ Optional route extras:
71
+
72
+ - ``validate="email"`` + ``variants={"invalid": ..., "valid": ...}`` for form demos
73
+ - ``validate="credentials"`` + ``variants`` (demo user ``ada`` / ``correct-horse``)
74
+ - ``sequence=(handler1, handler2, ...)`` for polling / multi-step GETs
75
+ - ``accumulate="field"`` + ``empty=...`` for append-only list demos (CRUD notes)
76
+ - ``list_remove=True`` on DELETE to drop one accumulated item by index
77
+ """
78
+
79
+ title: str = "Hedron sim"
80
+ demo_id: str | None = None
81
+ _page_path: str = field(default="/", init=False, repr=False)
82
+ _page_handler: Handler | None = field(default=None, init=False, repr=False)
83
+ _routes: dict[str, SimRoute] = field(
84
+ default_factory=lambda: {},
85
+ init=False,
86
+ repr=False,
87
+ )
88
+
89
+ def region(
90
+ self,
91
+ id: str,
92
+ *,
93
+ selector: str | None = None,
94
+ description: str = "",
95
+ ) -> FragmentRegion:
96
+ """Declare a fragment region (default selector ``#{id}``)."""
97
+ return FragmentRegion(id=id, selector=selector or f"#{id}", description=description)
98
+
99
+ def page(self, path: str = "/") -> Callable[[F], F]:
100
+ """Register the initial document body for the demo."""
101
+
102
+ def wrap(fn: F) -> F:
103
+ self._page_path = path
104
+ self._page_handler = fn
105
+ return fn
106
+
107
+ return wrap
108
+
109
+ def fragment(
110
+ self,
111
+ path: str,
112
+ *,
113
+ region: FragmentRegion | str | None = None,
114
+ regions: Sequence[FragmentRegion | str] | None = None,
115
+ fragment_regions: Sequence[FragmentRegion | str] | None = None,
116
+ method: str = "GET",
117
+ explanation: str = "",
118
+ validate: str | None = None,
119
+ variants: Mapping[str, Handler] | None = None,
120
+ sequence: Sequence[Handler] | None = None,
121
+ accumulate: str | None = None,
122
+ empty: Handler | None = None,
123
+ list_remove: bool = False,
124
+ ) -> Callable[[F], F]:
125
+ """Register a fragment endpoint with an optional region allowlist."""
126
+
127
+ def wrap(fn: F) -> F:
128
+ if accumulate and empty is None:
129
+ raise ValueError("accumulate=... requires empty= handler for the empty list UI")
130
+ route = SimRoute(
131
+ method=method.upper(),
132
+ path=path,
133
+ handler=fn,
134
+ regions=_as_regions(region, regions, fragment_regions),
135
+ explanation=explanation,
136
+ validate=validate,
137
+ variants=dict(variants) if variants else None,
138
+ sequence=tuple(sequence) if sequence else None,
139
+ accumulate=accumulate,
140
+ empty=empty,
141
+ list_remove=list_remove,
142
+ )
143
+ self._routes[route.key] = route
144
+ return fn
145
+
146
+ return wrap
147
+
148
+ def action(
149
+ self,
150
+ path: str,
151
+ *,
152
+ region: FragmentRegion | str | None = None,
153
+ regions: Sequence[FragmentRegion | str] | None = None,
154
+ fragment_regions: Sequence[FragmentRegion | str] | None = None,
155
+ method: str = "POST",
156
+ explanation: str = "",
157
+ validate: str | None = None,
158
+ variants: Mapping[str, Handler] | None = None,
159
+ sequence: Sequence[Handler] | None = None,
160
+ accumulate: str | None = None,
161
+ empty: Handler | None = None,
162
+ list_remove: bool = False,
163
+ ) -> Callable[[F], F]:
164
+ """Register a mutation endpoint (default POST) for form demos."""
165
+ return self.fragment(
166
+ path,
167
+ region=region,
168
+ regions=regions,
169
+ fragment_regions=fragment_regions,
170
+ method=method,
171
+ explanation=explanation,
172
+ validate=validate,
173
+ variants=variants,
174
+ sequence=sequence,
175
+ accumulate=accumulate,
176
+ empty=empty,
177
+ list_remove=list_remove,
178
+ )
179
+
180
+ @property
181
+ def page_handler(self) -> Handler | None:
182
+ return self._page_handler
183
+
184
+ @property
185
+ def page_path(self) -> str:
186
+ return self._page_path
187
+
188
+ @property
189
+ def routes(self) -> dict[str, SimRoute]:
190
+ return dict(self._routes)
@@ -0,0 +1,50 @@
1
+ """Locate and copy packaged JS/CSS assets into a docs tree."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import resources
6
+ from pathlib import Path
7
+
8
+ __all__ = ["asset_path", "copy_assets", "css_text", "javascript_text"]
9
+
10
+
11
+ def asset_path(name: str) -> Path:
12
+ """Return a filesystem path to a packaged static asset (may be inside a zip)."""
13
+ target = resources.files("hedron_sim").joinpath("static").joinpath(name)
14
+ with resources.as_file(target) as path:
15
+ return Path(path)
16
+
17
+
18
+ def javascript_text() -> str:
19
+ return (
20
+ resources.files("hedron_sim").joinpath("static/hedron-sim.js").read_text(encoding="utf-8")
21
+ )
22
+
23
+
24
+ def css_text() -> str:
25
+ return (
26
+ resources.files("hedron_sim").joinpath("static/hedron-sim.css").read_text(encoding="utf-8")
27
+ )
28
+
29
+
30
+ def copy_assets(
31
+ javascript_dir: Path | str,
32
+ stylesheets_dir: Path | str | None = None,
33
+ ) -> tuple[Path, Path | None]:
34
+ """Copy ``hedron-sim.js`` (and optional CSS) into docs asset directories.
35
+
36
+ Returns:
37
+ Paths to the written JS file and CSS file (CSS may be ``None`` when skipped).
38
+ """
39
+ js_dir = Path(javascript_dir)
40
+ js_dir.mkdir(parents=True, exist_ok=True)
41
+ js_dest = js_dir / "hedron-sim.js"
42
+ js_dest.write_text(javascript_text(), encoding="utf-8")
43
+
44
+ css_dest: Path | None = None
45
+ if stylesheets_dir is not None:
46
+ css_dir = Path(stylesheets_dir)
47
+ css_dir.mkdir(parents=True, exist_ok=True)
48
+ css_dest = css_dir / "hedron-sim.css"
49
+ css_dest.write_text(css_text(), encoding="utf-8")
50
+ return js_dest, css_dest