hedron-elements 0.36.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_elements-0.36.0/.gitignore +35 -0
- hedron_elements-0.36.0/CHANGELOG.md +6 -0
- hedron_elements-0.36.0/LICENSE +21 -0
- hedron_elements-0.36.0/PKG-INFO +48 -0
- hedron_elements-0.36.0/README.md +17 -0
- hedron_elements-0.36.0/pyproject.toml +69 -0
- hedron_elements-0.36.0/src/hedron_elements/__init__.py +22 -0
- hedron_elements-0.36.0/src/hedron_elements/assets.py +26 -0
- hedron_elements-0.36.0/src/hedron_elements/example.py +81 -0
- hedron_elements-0.36.0/src/hedron_elements/markup.py +171 -0
- hedron_elements-0.36.0/src/hedron_elements/plugin.py +141 -0
- hedron_elements-0.36.0/src/hedron_elements/py.typed +0 -0
- hedron_elements-0.36.0/src/hedron_elements/state.py +113 -0
- hedron_elements-0.36.0/src/hedron_elements/static/hedron-bridge.mjs +60 -0
- hedron_elements-0.36.0/src/hedron_elements/static/hedron-example.css +10 -0
- hedron_elements-0.36.0/src/hedron_elements/static/hedron-example.mjs +110 -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,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,48 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hedron-elements
|
|
3
|
+
Version: 0.36.0
|
|
4
|
+
Summary: Alpha Web Component ABI, shared bridge, and first-party elements for Hedron (RFC-0060)
|
|
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-elements/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: custom-elements,hedron,htmx,web-components
|
|
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: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: <3.15,>=3.11
|
|
29
|
+
Requires-Dist: hedron-core<0.37,>=0.36.0
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# hedron-elements
|
|
33
|
+
|
|
34
|
+
**Package maturity:** Alpha · **Train:** `0.36.x` · pin `>=0.36.0,<0.37`
|
|
35
|
+
|
|
36
|
+
Framework-neutral Web Component ABI, shared HTMX-safe bridge, and the `hedron-example`
|
|
37
|
+
reference element for Hedron (RFC-0060 / D-064).
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install "hedron[elements]>=0.36.0,<0.37"
|
|
41
|
+
# or
|
|
42
|
+
pip install "hedron-elements>=0.36.0,<0.37"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Depends on `hedron-core` only. Host applications do not need Node.js.
|
|
46
|
+
|
|
47
|
+
See the [implementation plan](https://github.com/eddiethedean/hedron/blob/main/docs/implementation/HEDRON_ELEMENTS_036.md)
|
|
48
|
+
and [platform spec](https://github.com/eddiethedean/hedron/blob/main/docs/implementation/WEB_COMPONENT_PLATFORM.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# hedron-elements
|
|
2
|
+
|
|
3
|
+
**Package maturity:** Alpha · **Train:** `0.36.x` · pin `>=0.36.0,<0.37`
|
|
4
|
+
|
|
5
|
+
Framework-neutral Web Component ABI, shared HTMX-safe bridge, and the `hedron-example`
|
|
6
|
+
reference element for Hedron (RFC-0060 / D-064).
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install "hedron[elements]>=0.36.0,<0.37"
|
|
10
|
+
# or
|
|
11
|
+
pip install "hedron-elements>=0.36.0,<0.37"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Depends on `hedron-core` only. Host applications do not need Node.js.
|
|
15
|
+
|
|
16
|
+
See the [implementation plan](https://github.com/eddiethedean/hedron/blob/main/docs/implementation/HEDRON_ELEMENTS_036.md)
|
|
17
|
+
and [platform spec](https://github.com/eddiethedean/hedron/blob/main/docs/implementation/WEB_COMPONENT_PLATFORM.md).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hedron-elements"
|
|
3
|
+
version = "0.36.0"
|
|
4
|
+
description = "Alpha Web Component ABI, shared bridge, and first-party elements for Hedron (RFC-0060)"
|
|
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
|
+
"hedron",
|
|
17
|
+
"web-components",
|
|
18
|
+
"htmx",
|
|
19
|
+
"custom-elements",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Programming Language :: Python",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
32
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"hedron-core>=0.36.0,<0.37",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.entry-points."hedron.plugins"]
|
|
41
|
+
hedron_elements = "hedron_elements.plugin:register"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/eddiethedean/hedron"
|
|
45
|
+
Repository = "https://github.com/eddiethedean/hedron"
|
|
46
|
+
Issues = "https://github.com/eddiethedean/hedron/issues"
|
|
47
|
+
Changelog = "https://github.com/eddiethedean/hedron/blob/main/packages/hedron-elements/CHANGELOG.md"
|
|
48
|
+
Documentation = "https://hedron.readthedocs.io/en/latest/"
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["hatchling"]
|
|
52
|
+
build-backend = "hatchling.build"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/hedron_elements"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
58
|
+
"src/hedron_elements/static/hedron-bridge.mjs" = "hedron_elements/static/hedron-bridge.mjs"
|
|
59
|
+
"src/hedron_elements/static/hedron-example.mjs" = "hedron_elements/static/hedron-example.mjs"
|
|
60
|
+
"src/hedron_elements/static/hedron-example.css" = "hedron_elements/static/hedron-example.css"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.sdist]
|
|
63
|
+
only-include = [
|
|
64
|
+
"src/hedron_elements",
|
|
65
|
+
"README.md",
|
|
66
|
+
"CHANGELOG.md",
|
|
67
|
+
"LICENSE",
|
|
68
|
+
"pyproject.toml",
|
|
69
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""hedron-elements: Alpha Web Component ABI and first-party elements."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from hedron_elements.example import Example
|
|
6
|
+
from hedron_elements.markup import render_element_markup
|
|
7
|
+
from hedron_elements.state import (
|
|
8
|
+
OwnershipMode,
|
|
9
|
+
apply_incoming_update,
|
|
10
|
+
validate_field_ownership,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.36.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Example",
|
|
17
|
+
"OwnershipMode",
|
|
18
|
+
"apply_incoming_update",
|
|
19
|
+
"render_element_markup",
|
|
20
|
+
"validate_field_ownership",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Locate packaged element static assets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
__all__ = ["asset_path", "bridge_path", "example_css_path", "example_module_path"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def asset_path(name: str) -> Path:
|
|
12
|
+
target = resources.files("hedron_elements").joinpath("static").joinpath(name)
|
|
13
|
+
with resources.as_file(target) as path:
|
|
14
|
+
return Path(path)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bridge_path() -> Path:
|
|
18
|
+
return asset_path("hedron-bridge.mjs")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def example_module_path() -> Path:
|
|
22
|
+
return asset_path("hedron-example.mjs")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def example_css_path() -> Path:
|
|
26
|
+
return asset_path("hedron-example.css")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Reference light-DOM element: hedron-example."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from hedron_core.component import Component, NodeLike
|
|
6
|
+
from hedron_core.html import html
|
|
7
|
+
from hedron_core.models import Props
|
|
8
|
+
from hedron_core.registry import ElementFieldOwnership
|
|
9
|
+
from hedron_elements.markup import render_element_markup
|
|
10
|
+
|
|
11
|
+
ABI_VERSION = 1
|
|
12
|
+
TAG_NAME = "hedron-example"
|
|
13
|
+
ELEMENT_ID = "hedron-example"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExampleProps(Props):
|
|
17
|
+
status: str = "Ready"
|
|
18
|
+
class_: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Example(Component[ExampleProps]):
|
|
22
|
+
"""ABI probe with controlled status text and disposable local UI."""
|
|
23
|
+
|
|
24
|
+
props_type = ExampleProps
|
|
25
|
+
logical_name = "Example"
|
|
26
|
+
distribution = "hedron-elements"
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
status: str = "Ready",
|
|
31
|
+
*,
|
|
32
|
+
class_: str | None = None,
|
|
33
|
+
**kwargs: object,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(ExampleProps(status=status, class_=class_, **kwargs))
|
|
36
|
+
|
|
37
|
+
def render(self) -> NodeLike:
|
|
38
|
+
return html.tag(TAG_NAME)(
|
|
39
|
+
html.p(self.props.status, **{"data-hedron-server-region": "content"}),
|
|
40
|
+
html.button("Details", type="button", **{"data-hedron-local": "toggle"}),
|
|
41
|
+
html.div(
|
|
42
|
+
"Local-only panel (disposable).",
|
|
43
|
+
hidden=True,
|
|
44
|
+
**{"data-hedron-local": "panel"},
|
|
45
|
+
),
|
|
46
|
+
**{
|
|
47
|
+
"data-hedron-abi": str(ABI_VERSION),
|
|
48
|
+
"data-hedron-element": ELEMENT_ID,
|
|
49
|
+
"status": self.props.status,
|
|
50
|
+
"class_": self.props.class_,
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def render_markup(self) -> str:
|
|
55
|
+
return render_element_markup(
|
|
56
|
+
tag_name=TAG_NAME,
|
|
57
|
+
abi_version=ABI_VERSION,
|
|
58
|
+
element_id=ELEMENT_ID,
|
|
59
|
+
attributes={"status": self.props.status},
|
|
60
|
+
server_content=self.props.status,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
EXAMPLE_OWNERSHIP: tuple[ElementFieldOwnership, ...] = (
|
|
65
|
+
ElementFieldOwnership(
|
|
66
|
+
name="status",
|
|
67
|
+
mode="controlled",
|
|
68
|
+
reflection="attribute",
|
|
69
|
+
incoming_update="replace",
|
|
70
|
+
persistence="none",
|
|
71
|
+
event="hedron-example-change",
|
|
72
|
+
),
|
|
73
|
+
ElementFieldOwnership(
|
|
74
|
+
name="expanded",
|
|
75
|
+
mode="local",
|
|
76
|
+
reflection="none",
|
|
77
|
+
incoming_update="preserve",
|
|
78
|
+
persistence="none",
|
|
79
|
+
event="hedron-example-change",
|
|
80
|
+
),
|
|
81
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Frozen element markup helpers (ABI-036)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from html import escape
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from hedron_core.diagnostics import error
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"MAX_STRUCTURED_BYTES",
|
|
15
|
+
"MAX_STRUCTURED_DEPTH",
|
|
16
|
+
"MAX_STRUCTURED_ITEMS",
|
|
17
|
+
"encode_structured_input",
|
|
18
|
+
"render_element_markup",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
MAX_STRUCTURED_BYTES = 8192
|
|
22
|
+
MAX_STRUCTURED_ITEMS = 64
|
|
23
|
+
MAX_STRUCTURED_DEPTH = 4
|
|
24
|
+
|
|
25
|
+
# Custom-element token: no quotes, spaces, ``>``, or ``/`` breakouts (#215).
|
|
26
|
+
_CUSTOM_ELEMENT_TAG = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)+$")
|
|
27
|
+
# Match hedron_core.html / _serializer attribute-name allowlist.
|
|
28
|
+
_SAFE_ATTR_NAME = re.compile(r"^[A-Za-z_][\w.-]*$")
|
|
29
|
+
_FROZEN_ATTR_NAMES = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"data-hedron-abi",
|
|
32
|
+
"data-hedron-element",
|
|
33
|
+
"data-hedron-input",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _strip_nul(value: str) -> str:
|
|
39
|
+
return value.replace("\x00", "")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _require_element_tag(tag_name: str) -> str:
|
|
43
|
+
tag = _strip_nul(tag_name).strip().lower()
|
|
44
|
+
if not _CUSTOM_ELEMENT_TAG.match(tag) or not tag.startswith("hedron-"):
|
|
45
|
+
raise error(
|
|
46
|
+
"HED-ELEMENT-0003",
|
|
47
|
+
title="Invalid element tag",
|
|
48
|
+
explanation=f"Tag {tag_name!r} is not a valid first-party element name.",
|
|
49
|
+
remediation="Use a hedron-* custom element tag matching [a-z][a-z0-9]*(-[a-z0-9]+)+.",
|
|
50
|
+
)
|
|
51
|
+
return tag
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _require_safe_attr_name(name: str) -> str:
|
|
55
|
+
cleaned = _strip_nul(name)
|
|
56
|
+
if not cleaned or not _SAFE_ATTR_NAME.match(cleaned) or any(ord(ch) < 32 for ch in cleaned):
|
|
57
|
+
raise error(
|
|
58
|
+
"HED-SEC-0010",
|
|
59
|
+
title="Unsafe attribute name rejected",
|
|
60
|
+
explanation=f"Attribute name {name!r} contains forbidden characters.",
|
|
61
|
+
remediation="Use token attribute names matching [A-Za-z_][\\w.-]*.",
|
|
62
|
+
)
|
|
63
|
+
lower = cleaned.lower()
|
|
64
|
+
if lower.startswith("on"):
|
|
65
|
+
raise error(
|
|
66
|
+
"HED-SEC-0002",
|
|
67
|
+
title="Inline event handler rejected",
|
|
68
|
+
explanation=f"Attribute {name!r} is an inline event handler.",
|
|
69
|
+
remediation="Use HTMX attributes or registered Web Components instead.",
|
|
70
|
+
)
|
|
71
|
+
return cleaned
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _depth(value: object, current: int = 0) -> int:
|
|
75
|
+
if current > MAX_STRUCTURED_DEPTH:
|
|
76
|
+
return current
|
|
77
|
+
if isinstance(value, Mapping):
|
|
78
|
+
return max((_depth(v, current + 1) for v in value.values()), default=current)
|
|
79
|
+
if isinstance(value, (list, tuple)):
|
|
80
|
+
return max((_depth(v, current + 1) for v in value), default=current)
|
|
81
|
+
return current
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def encode_structured_input(payload: Mapping[str, Any], *, instance_id: str) -> str:
|
|
85
|
+
"""Return an inert JSON script tag associated by instance id (never executed)."""
|
|
86
|
+
try:
|
|
87
|
+
raw = json.dumps(payload, separators=(",", ":"), allow_nan=False)
|
|
88
|
+
except (TypeError, ValueError) as exc:
|
|
89
|
+
raise error(
|
|
90
|
+
"HED-ELEMENT-0005",
|
|
91
|
+
title="Structured input encoding failed",
|
|
92
|
+
explanation=str(exc),
|
|
93
|
+
remediation="Provide JSON-serializable bounded configuration.",
|
|
94
|
+
) from exc
|
|
95
|
+
if len(raw.encode("utf-8")) > MAX_STRUCTURED_BYTES:
|
|
96
|
+
raise error(
|
|
97
|
+
"HED-ELEMENT-0005",
|
|
98
|
+
title="Structured input too large",
|
|
99
|
+
explanation=f"Payload exceeds {MAX_STRUCTURED_BYTES} bytes.",
|
|
100
|
+
remediation="Move large datasets to page/job endpoints.",
|
|
101
|
+
)
|
|
102
|
+
if isinstance(payload, Mapping) and len(payload) > MAX_STRUCTURED_ITEMS:
|
|
103
|
+
raise error(
|
|
104
|
+
"HED-ELEMENT-0005",
|
|
105
|
+
title="Structured input item limit",
|
|
106
|
+
explanation=f"Payload exceeds {MAX_STRUCTURED_ITEMS} items.",
|
|
107
|
+
remediation="Reduce configuration size.",
|
|
108
|
+
)
|
|
109
|
+
if _depth(payload) > MAX_STRUCTURED_DEPTH:
|
|
110
|
+
raise error(
|
|
111
|
+
"HED-ELEMENT-0005",
|
|
112
|
+
title="Structured input depth limit",
|
|
113
|
+
explanation=f"Payload exceeds depth {MAX_STRUCTURED_DEPTH}.",
|
|
114
|
+
remediation="Flatten configuration.",
|
|
115
|
+
)
|
|
116
|
+
safe_id = escape(_strip_nul(instance_id), quote=True)
|
|
117
|
+
# Escape closing tags inside JSON text nodes.
|
|
118
|
+
safe_json = raw.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
|
|
119
|
+
return f'<script type="application/json" data-hedron-input-for="{safe_id}">{safe_json}</script>'
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def render_element_markup(
|
|
123
|
+
*,
|
|
124
|
+
tag_name: str,
|
|
125
|
+
abi_version: int,
|
|
126
|
+
element_id: str,
|
|
127
|
+
attributes: Mapping[str, str] | None = None,
|
|
128
|
+
server_content: str = "",
|
|
129
|
+
instance_id: str | None = None,
|
|
130
|
+
structured_input: Mapping[str, Any] | None = None,
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Render frozen ABI markup for a light-DOM first-party element."""
|
|
133
|
+
tag = _require_element_tag(tag_name)
|
|
134
|
+
if abi_version < 1:
|
|
135
|
+
raise error(
|
|
136
|
+
"HED-ELEMENT-0002",
|
|
137
|
+
title="Invalid ABI version",
|
|
138
|
+
explanation=f"ABI {abi_version} is unsupported.",
|
|
139
|
+
remediation="Use a positive ABI major.",
|
|
140
|
+
)
|
|
141
|
+
# Caller attributes first; frozen ABI identity always wins afterward (#215).
|
|
142
|
+
attrs: dict[str, str] = {}
|
|
143
|
+
for key, value in dict(attributes or {}).items():
|
|
144
|
+
safe_key = _require_safe_attr_name(str(key))
|
|
145
|
+
if safe_key.lower() in _FROZEN_ATTR_NAMES:
|
|
146
|
+
raise error(
|
|
147
|
+
"HED-ELEMENT-0003",
|
|
148
|
+
title="Frozen element attribute rejected",
|
|
149
|
+
explanation=f"Attribute {safe_key!r} is reserved for ABI identity markup.",
|
|
150
|
+
remediation="Omit data-hedron-abi / data-hedron-element / data-hedron-input.",
|
|
151
|
+
)
|
|
152
|
+
attrs[safe_key] = _strip_nul(str(value))
|
|
153
|
+
attrs["data-hedron-abi"] = str(abi_version)
|
|
154
|
+
attrs["data-hedron-element"] = _strip_nul(element_id)
|
|
155
|
+
if instance_id:
|
|
156
|
+
attrs["data-hedron-input"] = _strip_nul(instance_id)
|
|
157
|
+
attr_html = " ".join(
|
|
158
|
+
f'{escape(k, quote=True)}="{escape(str(v), quote=True)}"' for k, v in attrs.items()
|
|
159
|
+
)
|
|
160
|
+
content = escape(_strip_nul(server_content))
|
|
161
|
+
body = f'<{tag} {attr_html}><p data-hedron-server-region="content">{content}</p></{tag}>'
|
|
162
|
+
if structured_input is not None:
|
|
163
|
+
if not instance_id:
|
|
164
|
+
raise error(
|
|
165
|
+
"HED-ELEMENT-0005",
|
|
166
|
+
title="Structured input requires instance id",
|
|
167
|
+
explanation="data-hedron-input association is required.",
|
|
168
|
+
remediation="Pass instance_id with structured_input.",
|
|
169
|
+
)
|
|
170
|
+
body = encode_structured_input(structured_input, instance_id=instance_id) + body
|
|
171
|
+
return body
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Register hedron-elements plugin: ABI, assets, Example component."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hedron_core.identifiers import content_digest
|
|
8
|
+
from hedron_core.plugins import PluginCapabilities, PluginContext, PluginMeta
|
|
9
|
+
from hedron_core.registry import (
|
|
10
|
+
register_asset,
|
|
11
|
+
register_browser_module,
|
|
12
|
+
register_component,
|
|
13
|
+
register_element_definition,
|
|
14
|
+
)
|
|
15
|
+
from hedron_elements.example import (
|
|
16
|
+
ABI_VERSION,
|
|
17
|
+
ELEMENT_ID,
|
|
18
|
+
EXAMPLE_OWNERSHIP,
|
|
19
|
+
TAG_NAME,
|
|
20
|
+
Example,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_ROOT = Path(__file__).resolve().parent
|
|
24
|
+
_BRIDGE = _ROOT / "static" / "hedron-bridge.mjs"
|
|
25
|
+
_JS = _ROOT / "static" / "hedron-example.mjs"
|
|
26
|
+
_CSS = _ROOT / "static" / "hedron-example.css"
|
|
27
|
+
|
|
28
|
+
PLUGIN_META = PluginMeta(
|
|
29
|
+
name="hedron_elements",
|
|
30
|
+
version="0.36.0",
|
|
31
|
+
distribution="hedron-elements",
|
|
32
|
+
hedron_version=">=0.36,<0.37",
|
|
33
|
+
capabilities=PluginCapabilities(
|
|
34
|
+
python=True,
|
|
35
|
+
styles=True,
|
|
36
|
+
assets=True,
|
|
37
|
+
browser_js=True,
|
|
38
|
+
explorer_panels=False,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def register(ctx: PluginContext) -> None:
|
|
44
|
+
logical = (
|
|
45
|
+
f"{Example.distribution}:{Example.__module__}."
|
|
46
|
+
f"{getattr(Example, 'logical_name', Example.__name__)}"
|
|
47
|
+
)
|
|
48
|
+
register_component(
|
|
49
|
+
logical_id=logical,
|
|
50
|
+
name=getattr(Example, "logical_name", Example.__name__) or Example.__name__,
|
|
51
|
+
module=Example.__module__,
|
|
52
|
+
distribution=Example.distribution,
|
|
53
|
+
props_model=Example.props_type.__name__,
|
|
54
|
+
browser_modules=(str(_JS),) if _JS.is_file() else (),
|
|
55
|
+
accessibility_notes=(
|
|
56
|
+
"Light-DOM status region with progressive-enhancement toggle; "
|
|
57
|
+
"usable before upgrade and after module failure."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if _BRIDGE.is_file():
|
|
62
|
+
digest = content_digest(_BRIDGE.read_bytes())
|
|
63
|
+
register_asset(
|
|
64
|
+
logical_id="hedron-elements:bridge.mjs",
|
|
65
|
+
kind="module",
|
|
66
|
+
path=str(_BRIDGE),
|
|
67
|
+
digest=digest,
|
|
68
|
+
content_type="text/javascript",
|
|
69
|
+
)
|
|
70
|
+
if _JS.is_file():
|
|
71
|
+
digest = content_digest(_JS.read_bytes())
|
|
72
|
+
register_asset(
|
|
73
|
+
logical_id="hedron-elements:example.mjs",
|
|
74
|
+
kind="module",
|
|
75
|
+
path=str(_JS),
|
|
76
|
+
digest=digest,
|
|
77
|
+
content_type="text/javascript",
|
|
78
|
+
)
|
|
79
|
+
register_browser_module(
|
|
80
|
+
logical_id="hedron-elements:example",
|
|
81
|
+
tag_name=TAG_NAME,
|
|
82
|
+
module_path=str(_JS),
|
|
83
|
+
observed_attributes=("status", "data-hedron-abi", "data-hedron-element"),
|
|
84
|
+
events=("hedron-example-change",),
|
|
85
|
+
shadow_dom=False,
|
|
86
|
+
htmx_lifecycle=True,
|
|
87
|
+
)
|
|
88
|
+
register_element_definition(
|
|
89
|
+
logical_id=ELEMENT_ID,
|
|
90
|
+
tag_name=TAG_NAME,
|
|
91
|
+
abi_version=ABI_VERSION,
|
|
92
|
+
module_asset_id="hedron-elements:example.mjs",
|
|
93
|
+
attributes=("status", "data-hedron-abi", "data-hedron-element"),
|
|
94
|
+
state_ownership=EXAMPLE_OWNERSHIP,
|
|
95
|
+
events=("hedron-example-change",),
|
|
96
|
+
dom_policy="light",
|
|
97
|
+
server_regions=("content",),
|
|
98
|
+
form_contract=None,
|
|
99
|
+
a11y_contract={
|
|
100
|
+
"role": "group",
|
|
101
|
+
"name": "Example status",
|
|
102
|
+
"keyboard": "button toggle",
|
|
103
|
+
},
|
|
104
|
+
style_contract={"tokens": "hedron-*"},
|
|
105
|
+
resources=("hedron-elements:bridge.mjs", "hedron-elements:example.css"),
|
|
106
|
+
lifecycle={
|
|
107
|
+
"connect": "idempotent",
|
|
108
|
+
"disconnect": "abort+dispose",
|
|
109
|
+
"htmx": "beforeCleanupElement",
|
|
110
|
+
},
|
|
111
|
+
fallback={
|
|
112
|
+
"pre_upgrade": "server content visible",
|
|
113
|
+
"js_off": "server content visible",
|
|
114
|
+
"module_failure": "retain server content",
|
|
115
|
+
},
|
|
116
|
+
first_party=True,
|
|
117
|
+
)
|
|
118
|
+
if _CSS.is_file():
|
|
119
|
+
digest = content_digest(_CSS.read_bytes())
|
|
120
|
+
register_asset(
|
|
121
|
+
logical_id="hedron-elements:example.css",
|
|
122
|
+
kind="css",
|
|
123
|
+
path=str(_CSS),
|
|
124
|
+
digest=digest,
|
|
125
|
+
content_type="text/css",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
ctx.register_diagnostic_owner("HED-ELEMENT-")
|
|
129
|
+
ctx.register_feature(
|
|
130
|
+
name="web_component_abi",
|
|
131
|
+
stability="experimental",
|
|
132
|
+
description="Versioned element ABI, SSR fallback, and HTMX lifecycle foundation.",
|
|
133
|
+
http_fallback=True,
|
|
134
|
+
security_notes=(
|
|
135
|
+
"Custom-element events are untrusted; Shadow DOM is not a security boundary; "
|
|
136
|
+
"CSRF/authz remain server-owned."
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
register.PLUGIN_META = PLUGIN_META # type: ignore[attr-defined]
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""ElementStateOwnership helpers (STATE-036)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from hedron_core.diagnostics import error
|
|
8
|
+
from hedron_core.registry import ElementFieldOwnership
|
|
9
|
+
|
|
10
|
+
OwnershipMode = Literal["controlled", "local", "draft", "preference"]
|
|
11
|
+
IncomingPolicy = Literal["replace", "preserve", "rebase", "conflict"]
|
|
12
|
+
|
|
13
|
+
_CAPABILITY_FIELDS = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"auth",
|
|
16
|
+
"authorization",
|
|
17
|
+
"csrf",
|
|
18
|
+
"secret",
|
|
19
|
+
"token",
|
|
20
|
+
"capability",
|
|
21
|
+
"trusted_html",
|
|
22
|
+
"tenant",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"IncomingPolicy",
|
|
28
|
+
"OwnershipMode",
|
|
29
|
+
"apply_incoming_update",
|
|
30
|
+
"refuse_transfer",
|
|
31
|
+
"validate_field_ownership",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_field_ownership(field: ElementFieldOwnership) -> ElementFieldOwnership:
|
|
36
|
+
if field.mode not in {"controlled", "local", "draft", "preference"}:
|
|
37
|
+
raise error(
|
|
38
|
+
"HED-ELEMENT-STATE-0001",
|
|
39
|
+
title="Unknown ownership mode",
|
|
40
|
+
explanation=f"Field {field.name!r} has invalid mode {field.mode!r}.",
|
|
41
|
+
remediation="Use controlled, local, draft, or preference.",
|
|
42
|
+
)
|
|
43
|
+
lowered = field.name.lower()
|
|
44
|
+
if any(token in lowered for token in _CAPABILITY_FIELDS) and field.mode != "controlled":
|
|
45
|
+
raise error(
|
|
46
|
+
"HED-ELEMENT-STATE-0002",
|
|
47
|
+
title="Illegal element-owned capability",
|
|
48
|
+
explanation=f"Field {field.name!r} cannot use mode {field.mode!r}.",
|
|
49
|
+
remediation="Keep capabilities under server-controlled ownership.",
|
|
50
|
+
)
|
|
51
|
+
if field.persistence not in {"none", "preference", "draft"}:
|
|
52
|
+
raise error(
|
|
53
|
+
"HED-ELEMENT-STATE-0002",
|
|
54
|
+
title="Illegal persistence policy",
|
|
55
|
+
explanation=f"Persistence {field.persistence!r} is not allowed.",
|
|
56
|
+
remediation="Use none, preference, or draft persistence.",
|
|
57
|
+
)
|
|
58
|
+
if field.persistence == "preference" and field.mode != "preference":
|
|
59
|
+
raise error(
|
|
60
|
+
"HED-ELEMENT-STATE-0002",
|
|
61
|
+
title="Illegal persistence for ownership mode",
|
|
62
|
+
explanation="Only preference fields may persist as preference.",
|
|
63
|
+
remediation="Align persistence with ownership mode.",
|
|
64
|
+
)
|
|
65
|
+
return field
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def apply_incoming_update(
|
|
69
|
+
*,
|
|
70
|
+
mode: OwnershipMode,
|
|
71
|
+
dirty: bool,
|
|
72
|
+
policy: IncomingPolicy | None,
|
|
73
|
+
allow_rebase: bool = False,
|
|
74
|
+
) -> IncomingPolicy:
|
|
75
|
+
"""Resolve dirty-draft / controlled incoming-update policy (no silent LWW)."""
|
|
76
|
+
if mode == "local":
|
|
77
|
+
return "preserve"
|
|
78
|
+
if mode == "controlled":
|
|
79
|
+
return "replace"
|
|
80
|
+
if mode == "preference":
|
|
81
|
+
return policy or "preserve"
|
|
82
|
+
# draft
|
|
83
|
+
if not dirty:
|
|
84
|
+
return "replace"
|
|
85
|
+
chosen = policy or "conflict"
|
|
86
|
+
if chosen == "rebase" and not allow_rebase:
|
|
87
|
+
raise error(
|
|
88
|
+
"HED-ELEMENT-STATE-0004",
|
|
89
|
+
title="Unproven draft rebase",
|
|
90
|
+
explanation="Rebase requires a proven typed merge.",
|
|
91
|
+
remediation="Use conflict, preserve, or a proven rebase.",
|
|
92
|
+
)
|
|
93
|
+
if chosen not in {"replace", "preserve", "rebase", "conflict"}:
|
|
94
|
+
raise error(
|
|
95
|
+
"HED-ELEMENT-STATE-0004",
|
|
96
|
+
title="Missing draft incoming policy",
|
|
97
|
+
explanation=f"Policy {chosen!r} is not supported.",
|
|
98
|
+
remediation="Declare replace, preserve, rebase, or conflict.",
|
|
99
|
+
)
|
|
100
|
+
if chosen == "conflict":
|
|
101
|
+
# Explicit conflict path — never last-write-wins.
|
|
102
|
+
return "conflict"
|
|
103
|
+
return chosen
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def refuse_transfer() -> None:
|
|
107
|
+
"""Cross-instance draft transfer is out of scope until phase 0.40."""
|
|
108
|
+
raise error(
|
|
109
|
+
"HED-ELEMENT-STATE-0006",
|
|
110
|
+
title="Draft transfer not available",
|
|
111
|
+
explanation="Cross-instance draft transfer is deferred to phase 0.40.",
|
|
112
|
+
remediation="Keep draft state instance-local until STATE-040.",
|
|
113
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Hedron element bridge (phase 0.36).
|
|
3
|
+
* Owns early HTMX cleanup hooks and typed-event allowlisting.
|
|
4
|
+
* Keep gzip size ≤ 12 KiB.
|
|
5
|
+
*/
|
|
6
|
+
const OWNED = new WeakMap();
|
|
7
|
+
|
|
8
|
+
export function track(el, resources) {
|
|
9
|
+
const bag = OWNED.get(el) || { ac: null, listeners: [], timers: [] };
|
|
10
|
+
if (!bag.ac) bag.ac = new AbortController();
|
|
11
|
+
OWNED.set(el, bag);
|
|
12
|
+
if (resources?.listener) bag.listeners.push(resources.listener);
|
|
13
|
+
if (resources?.timer != null) bag.timers.push(resources.timer);
|
|
14
|
+
return bag.ac.signal;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function dispose(el) {
|
|
18
|
+
const bag = OWNED.get(el);
|
|
19
|
+
if (!bag) return;
|
|
20
|
+
try {
|
|
21
|
+
bag.ac?.abort();
|
|
22
|
+
} catch (_) {}
|
|
23
|
+
for (const t of bag.timers || []) clearTimeout(t);
|
|
24
|
+
bag.timers = [];
|
|
25
|
+
bag.listeners = [];
|
|
26
|
+
OWNED.delete(el);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateEventDetail(detail, schemaKeys) {
|
|
30
|
+
if (detail == null || typeof detail !== "object") return false;
|
|
31
|
+
if ("__proto__" in detail || "constructor" in Object.getOwnPropertyDescriptors(detail || {})) {
|
|
32
|
+
// still allow normal objects; reject dangerous keys
|
|
33
|
+
}
|
|
34
|
+
for (const key of Object.keys(detail)) {
|
|
35
|
+
if (schemaKeys && !schemaKeys.includes(key)) return false;
|
|
36
|
+
const v = detail[key];
|
|
37
|
+
if (typeof v === "function") return false;
|
|
38
|
+
if (typeof Node !== "undefined" && v instanceof Node) return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function onBeforeCleanup(ev) {
|
|
44
|
+
const root = ev?.detail?.elt || ev?.target;
|
|
45
|
+
if (!root) return;
|
|
46
|
+
if (root.tagName && String(root.tagName).toLowerCase().startsWith("hedron-")) {
|
|
47
|
+
dispose(root);
|
|
48
|
+
}
|
|
49
|
+
root.querySelectorAll?.("[data-hedron-element]").forEach((node) => dispose(node));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof document !== "undefined") {
|
|
53
|
+
document.body?.addEventListener?.("htmx:beforeCleanupElement", onBeforeCleanup);
|
|
54
|
+
document.addEventListener("htmx:beforeSwap", (ev) => {
|
|
55
|
+
const t = ev.target || document;
|
|
56
|
+
t.querySelectorAll?.("[data-hedron-element]").forEach((node) => dispose(node));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const HEDRON_BRIDGE_VERSION = 1;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hedron-example — 0.36 ABI probe (light DOM).
|
|
3
|
+
* Controlled `status` + disposable local expanded UI. Not form-associated.
|
|
4
|
+
*/
|
|
5
|
+
import { dispose, track, validateEventDetail } from "./hedron-bridge.mjs";
|
|
6
|
+
|
|
7
|
+
const TAG = "hedron-example";
|
|
8
|
+
const EVENT = "hedron-example-change";
|
|
9
|
+
|
|
10
|
+
class HedronExample extends HTMLElement {
|
|
11
|
+
static observedAttributes = ["status", "data-hedron-abi"];
|
|
12
|
+
|
|
13
|
+
#signal = null;
|
|
14
|
+
#onToggle = null;
|
|
15
|
+
#expanded = false;
|
|
16
|
+
|
|
17
|
+
connectedCallback() {
|
|
18
|
+
this.#ensure();
|
|
19
|
+
this.#signal = track(this);
|
|
20
|
+
this.#bind();
|
|
21
|
+
this.#sync();
|
|
22
|
+
this.setAttribute("data-hedron-upgraded", "true");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
disconnectedCallback() {
|
|
26
|
+
this.#unbind();
|
|
27
|
+
dispose(this);
|
|
28
|
+
this.#signal = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
attributeChangedCallback() {
|
|
32
|
+
if (this.isConnected) this.#sync();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#ensure() {
|
|
36
|
+
if (!this.querySelector(":scope > [data-hedron-server-region='content']")) {
|
|
37
|
+
const p = document.createElement("p");
|
|
38
|
+
p.setAttribute("data-hedron-server-region", "content");
|
|
39
|
+
p.textContent = this.getAttribute("status") || "Ready";
|
|
40
|
+
this.appendChild(p);
|
|
41
|
+
}
|
|
42
|
+
if (!this.querySelector(":scope > [data-hedron-local='toggle']")) {
|
|
43
|
+
const btn = document.createElement("button");
|
|
44
|
+
btn.type = "button";
|
|
45
|
+
btn.setAttribute("data-hedron-local", "toggle");
|
|
46
|
+
btn.textContent = "Details";
|
|
47
|
+
this.appendChild(btn);
|
|
48
|
+
}
|
|
49
|
+
if (!this.querySelector(":scope > [data-hedron-local='panel']")) {
|
|
50
|
+
const panel = document.createElement("div");
|
|
51
|
+
panel.setAttribute("data-hedron-local", "panel");
|
|
52
|
+
panel.hidden = true;
|
|
53
|
+
panel.textContent = "Local-only panel (disposable).";
|
|
54
|
+
this.appendChild(panel);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#unbind() {
|
|
59
|
+
const btn = this.querySelector(":scope > [data-hedron-local='toggle']");
|
|
60
|
+
if (btn && this.#onToggle) btn.removeEventListener("click", this.#onToggle);
|
|
61
|
+
this.#onToggle = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#bind() {
|
|
65
|
+
this.#unbind();
|
|
66
|
+
const btn = this.querySelector(":scope > [data-hedron-local='toggle']");
|
|
67
|
+
if (!btn) return;
|
|
68
|
+
this.#onToggle = () => {
|
|
69
|
+
this.#expanded = !this.#expanded;
|
|
70
|
+
this.#syncLocal();
|
|
71
|
+
const detail = { expanded: this.#expanded, status: this.getAttribute("status") };
|
|
72
|
+
if (!validateEventDetail(detail, ["expanded", "status"])) return;
|
|
73
|
+
this.dispatchEvent(
|
|
74
|
+
new CustomEvent(EVENT, { bubbles: true, composed: false, detail }),
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
btn.addEventListener("click", this.#onToggle, { signal: this.#signal });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#sync() {
|
|
81
|
+
const region = this.querySelector(":scope > [data-hedron-server-region='content']");
|
|
82
|
+
if (region) region.textContent = this.getAttribute("status") || "Ready";
|
|
83
|
+
this.#syncLocal();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#syncLocal() {
|
|
87
|
+
const panel = this.querySelector(":scope > [data-hedron-local='panel']");
|
|
88
|
+
const btn = this.querySelector(":scope > [data-hedron-local='toggle']");
|
|
89
|
+
if (panel) panel.hidden = !this.#expanded;
|
|
90
|
+
if (btn) btn.setAttribute("aria-expanded", this.#expanded ? "true" : "false");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!customElements.get(TAG)) {
|
|
95
|
+
customElements.define(TAG, HedronExample);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function enhance(root) {
|
|
99
|
+
const nodes = [];
|
|
100
|
+
if (root?.matches?.(TAG)) nodes.push(root);
|
|
101
|
+
root?.querySelectorAll?.(TAG)?.forEach((el) => nodes.push(el));
|
|
102
|
+
for (const el of nodes) {
|
|
103
|
+
if (el instanceof HedronExample && el.isConnected) {
|
|
104
|
+
el.connectedCallback();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
document.addEventListener("htmx:afterSwap", (ev) => enhance(ev.target || document));
|
|
110
|
+
document.addEventListener("htmx:load", (ev) => enhance(ev.target || document));
|