"""Custom-element builder, registry, and TypeScript codegen. A custom element is a light-DOM Web Component: the Python builder emits a semantic tag whose typed props become kebab-case attributes and whose behavior lives in a compiled TS module (loaded via Media). One ``TypedDict`` per element is the single source of truth for the server<->client contract; ``gen_element_types`` turns each registered spec into a TS interface + attribute reader so drift fails ``tsc``. """ from dataclasses import dataclass from typing import TypedDict, get_type_hints from common.components.core import Media from common.components.primitives import custom_element_builder @dataclass(frozen=True) class ElementSpec: tag: str # e.g. "game-status-selector" ts_name: str # e.g. "GameStatusSelector" props: type # a TypedDict subclass ELEMENT_REGISTRY: list[ElementSpec] = [] def register_element(tag: str, ts_name: str, props: type) -> None: """Register an element so codegen can emit its TS contract.""" ELEMENT_REGISTRY.append(ElementSpec(tag, ts_name, props)) def _kebab(name: str) -> str: return name.replace("_", "-") # ── Codegen ────────────────────────────────────────────────────────────────── _TYPE_MAP = {int: "number", float: "number", str: "string", bool: "boolean"} def _camel(name: str) -> str: head, *tail = name.split("_") return head + "".join(part.title() for part in tail) def _reader_expr(name: str, python_type: type) -> str: attr = _kebab(name) if python_type in (int, float): return f'Number(el.getAttribute("{attr}"))' if python_type is bool: return f'el.getAttribute("{attr}") === "true"' return f'el.getAttribute("{attr}") ?? ""' def _ts_for_spec(spec: ElementSpec) -> str: hints = get_type_hints(spec.props) interface_lines = "\n".join( f" {_camel(name)}: {_TYPE_MAP[python_type]};" for name, python_type in hints.items() ) reader_lines = "\n".join( f" {_camel(name)}: {_reader_expr(name, python_type)}," for name, python_type in hints.items() ) return ( f"export interface {spec.ts_name}Props {{\n{interface_lines}\n}}\n\n" f"export function read{spec.ts_name}Props(el: HTMLElement): " f"{spec.ts_name}Props {{\n return {{\n{reader_lines}\n }};\n}}" ) def render_props_module() -> str: """The full ``ts/generated/props.ts`` content for every registered element.""" header = "// GENERATED by `manage.py gen_element_types` — do not edit.\n" blocks = [_ts_for_spec(spec) for spec in ELEMENT_REGISTRY] return header + "\n" + "\n\n".join(blocks) + "\n" # ── Element prop schemas (registered at import time) ───────────────────────── class GameStatusSelectorProps(TypedDict): game_id: int status: str csrf: str register_element("game-status-selector", "GameStatusSelector", GameStatusSelectorProps) class SessionDeviceSelectorProps(TypedDict): session_id: int csrf: str register_element( "session-device-selector", "SessionDeviceSelector", SessionDeviceSelectorProps ) class PlayEventRowProps(TypedDict): game_id: int csrf: str api_create_url: str 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")