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.
- hedron_sim-0.1.0/.gitignore +35 -0
- hedron_sim-0.1.0/CHANGELOG.md +51 -0
- hedron_sim-0.1.0/LICENSE +21 -0
- hedron_sim-0.1.0/PKG-INFO +128 -0
- hedron_sim-0.1.0/README.md +98 -0
- hedron_sim-0.1.0/pyproject.toml +65 -0
- hedron_sim-0.1.0/src/hedron_sim/__init__.py +22 -0
- hedron_sim-0.1.0/src/hedron_sim/app.py +190 -0
- hedron_sim-0.1.0/src/hedron_sim/assets.py +50 -0
- hedron_sim-0.1.0/src/hedron_sim/embed.py +183 -0
- hedron_sim-0.1.0/src/hedron_sim/py.typed +0 -0
- hedron_sim-0.1.0/src/hedron_sim/static/hedron-sim.css +330 -0
- hedron_sim-0.1.0/src/hedron_sim/static/hedron-sim.js +652 -0
- hedron_sim-0.1.0/src/hedron_sim/tokens.py +32 -0
|
@@ -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.
|
hedron_sim-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/hedron-sim/)
|
|
34
|
+
[](https://pypi.org/project/hedron-sim/)
|
|
35
|
+
[](https://github.com/eddiethedean/hedron/actions/workflows/ci.yml)
|
|
36
|
+
[](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
|
+
[](https://pypi.org/project/hedron-sim/)
|
|
4
|
+
[](https://pypi.org/project/hedron-sim/)
|
|
5
|
+
[](https://github.com/eddiethedean/hedron/actions/workflows/ci.yml)
|
|
6
|
+
[](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
|