Try unifying 3 different element interfaces
This commit is contained in:
@@ -18,7 +18,7 @@ from common.components.core import (
|
||||
randomid,
|
||||
render,
|
||||
)
|
||||
from common.components.custom_elements import custom_element, register_element
|
||||
from common.components.custom_elements import SessionTimestampButtons, register_element
|
||||
from common.components.date_range_picker import (
|
||||
DateRangeCalendar,
|
||||
DateRangeField,
|
||||
@@ -77,6 +77,7 @@ from common.components.primitives import (
|
||||
Tr,
|
||||
Ul,
|
||||
YearPicker,
|
||||
custom_element_builder,
|
||||
paginated_table_content,
|
||||
)
|
||||
from common.components.search_select import (
|
||||
@@ -92,8 +93,9 @@ from common.utils import truncate
|
||||
__all__ = [
|
||||
"truncate",
|
||||
"BaseComponent",
|
||||
"custom_element",
|
||||
"register_element",
|
||||
"SessionTimestampButtons",
|
||||
"custom_element_builder",
|
||||
"Element",
|
||||
"Fragment",
|
||||
"Media",
|
||||
|
||||
@@ -9,9 +9,10 @@ reader so drift fails ``tsc``.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Mapping, TypedDict, get_type_hints
|
||||
from typing import TypedDict, get_type_hints
|
||||
|
||||
from common.components.core import Children, Element, HTMLAttribute, Media, Node
|
||||
from common.components.core import Media
|
||||
from common.components.primitives import custom_element_builder
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -33,22 +34,6 @@ def _kebab(name: str) -> str:
|
||||
return name.replace("_", "-")
|
||||
|
||||
|
||||
def custom_element(
|
||||
tag: str, props: Mapping[str, object], *, children: Children = None
|
||||
) -> Node:
|
||||
"""Emit ``<tag kebab-attrs>children</tag>`` and declare its compiled module.
|
||||
|
||||
The module path mirrors the source layout: ``ts/elements/<tag>.ts`` compiles
|
||||
to ``dist/elements/<tag>.js``, which ``Media`` loads via ``ModuleScript``."""
|
||||
attributes: list[HTMLAttribute] = [
|
||||
(_kebab(key), value) # type: ignore[misc]
|
||||
for key, value in props.items()
|
||||
]
|
||||
return Element(tag, attributes, children).with_media(
|
||||
Media(js=(f"dist/elements/{tag}.js",))
|
||||
)
|
||||
|
||||
|
||||
# ── Codegen ──────────────────────────────────────────────────────────────────
|
||||
|
||||
_TYPE_MAP = {int: "number", float: "number", str: "string", bool: "boolean"}
|
||||
@@ -121,3 +106,22 @@ class PlayEventRowProps(TypedDict):
|
||||
|
||||
|
||||
register_element("play-event-row", "PlayEventRow", PlayEventRowProps)
|
||||
|
||||
|
||||
class SessionTimestampButtonsProps(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
register_element(
|
||||
"session-timestamp-buttons", "SessionTimestampButtons", SessionTimestampButtonsProps
|
||||
)
|
||||
|
||||
|
||||
# ── Named tag builders (consistent htpy-style with Div/Span) ─────────────────
|
||||
# Underscore-prefixed: used internally by domain wrappers.
|
||||
# Public ones (no domain wrapper): exported directly.
|
||||
|
||||
_GameStatusSelector = custom_element_builder("game-status-selector")
|
||||
_SessionDeviceSelector = custom_element_builder("session-device-selector")
|
||||
_PlayEventRow = custom_element_builder("play-event-row")
|
||||
SessionTimestampButtons = custom_element_builder("session-timestamp-buttons")
|
||||
|
||||
@@ -228,9 +228,8 @@ _SELECTOR_OPTION_CLASS = (
|
||||
|
||||
def GameStatusSelector(game, game_statuses, csrf_token: str) -> Node:
|
||||
"""Light-DOM custom element; behavior in ts/elements/game-status-selector.ts."""
|
||||
from common.components import custom_element
|
||||
from common.components.core import Element
|
||||
from common.components.custom_elements import GameStatusSelectorProps
|
||||
from common.components.custom_elements import _GameStatusSelector, GameStatusSelectorProps
|
||||
from common.components.primitives import Li, Ul
|
||||
|
||||
options = [
|
||||
@@ -266,18 +265,15 @@ def GameStatusSelector(game, game_statuses, csrf_token: str) -> Node:
|
||||
dropdown = Div(
|
||||
data_dropdown="", class_="inline-flex rounded-md shadow-2xs relative"
|
||||
)[toggle, menu]
|
||||
return custom_element(
|
||||
"game-status-selector",
|
||||
GameStatusSelectorProps(game_id=game.id, status=game.status, csrf=csrf_token),
|
||||
children=[Div(class_="flex gap-2 items-center")[dropdown]],
|
||||
)
|
||||
return _GameStatusSelector(game_id=game.id, status=game.status, csrf=csrf_token)[
|
||||
Div(class_="flex gap-2 items-center")[dropdown]
|
||||
]
|
||||
|
||||
|
||||
def SessionDeviceSelector(session, session_devices, csrf_token: str) -> Node:
|
||||
"""Light-DOM custom element; behavior in ts/elements/session-device-selector.ts."""
|
||||
from common.components import custom_element
|
||||
from common.components.core import Element
|
||||
from common.components.custom_elements import SessionDeviceSelectorProps
|
||||
from common.components.custom_elements import _SessionDeviceSelector, SessionDeviceSelectorProps
|
||||
from common.components.primitives import Li, Ul
|
||||
|
||||
current_name = session.device.name if session.device else "Unknown"
|
||||
@@ -307,8 +303,6 @@ def SessionDeviceSelector(session, session_devices, csrf_token: str) -> Node:
|
||||
dropdown = Div(
|
||||
data_dropdown="", class_="inline-flex rounded-md shadow-2xs relative"
|
||||
)[toggle, menu]
|
||||
return custom_element(
|
||||
"session-device-selector",
|
||||
SessionDeviceSelectorProps(session_id=session.id, csrf=csrf_token),
|
||||
children=[Div(class_="flex gap-2 items-center")[dropdown]],
|
||||
)
|
||||
return _SessionDeviceSelector(session_id=session.id, csrf=csrf_token)[
|
||||
Div(class_="flex gap-2 items-center")[dropdown]
|
||||
]
|
||||
|
||||
@@ -68,8 +68,21 @@ def _attrs_from_kwargs(attrs: dict[str, object]) -> list[HTMLAttribute]:
|
||||
return result
|
||||
|
||||
|
||||
def _html_element(tag_name: str):
|
||||
"""Build a generic element builder for ``tag_name`` (the whitelist factory)."""
|
||||
def custom_element_builder(tag_name: str):
|
||||
"""Create a tag builder for a custom element with auto-attached Media.
|
||||
|
||||
The module path follows the convention ``ts/elements/<tag>.ts`` →
|
||||
``dist/elements/<tag>.js``.
|
||||
"""
|
||||
return _html_element(tag_name, Media(js=(f"dist/elements/{tag_name}.js",)))
|
||||
|
||||
|
||||
def _html_element(tag_name: str, media: Media | None = None):
|
||||
"""Build a generic element builder for ``tag_name`` (the whitelist factory).
|
||||
|
||||
If ``media`` is provided, every node created by the builder will carry it
|
||||
(used for custom elements whose compiled JS must be loaded automatically).
|
||||
"""
|
||||
|
||||
def element(
|
||||
attributes: Attributes | None = None,
|
||||
@@ -77,7 +90,8 @@ def _html_element(tag_name: str):
|
||||
**attrs: object,
|
||||
) -> Element:
|
||||
merged = as_attributes(attributes) + _attrs_from_kwargs(attrs)
|
||||
return Element(tag_name, merged, children)
|
||||
node = Element(tag_name, merged, children)
|
||||
return node.with_media(media) if media else node
|
||||
|
||||
element.__name__ = element.__qualname__ = tag_name[:1].upper() + tag_name[1:]
|
||||
element.__doc__ = f"Builder for the <{tag_name}> element."
|
||||
@@ -245,8 +259,9 @@ def Button(
|
||||
title: str = "",
|
||||
onclick: str = "",
|
||||
name: str = "",
|
||||
**attrs: object,
|
||||
) -> Element:
|
||||
attributes = as_attributes(attributes)
|
||||
attributes = as_attributes(attributes) + _attrs_from_kwargs(attrs)
|
||||
children = children or []
|
||||
|
||||
# Separate custom class from other generic attributes
|
||||
|
||||
Reference in New Issue
Block a user