Custom elements + htpy-style HTML: kill the inline HTML/JS f-strings#16
Merged
Conversation
The Game status dropdown is now a <game-status-selector> light-DOM custom
element: the Python builder emits the tag + kebab attrs htpy-style, behavior
lives in ts/elements/{dropdown,game-status-selector}.ts wired by the native
connectedCallback, and GameStatusSelectorProps is the codegen'd contract. The
~70-line inline-Alpine f-string is gone.
Also fix SimpleTable to collect and re-attach the media of its row/header
nodes: it stringifies cells into the table markup, which silently dropped each
cell component's declared Media — so a <game-status-selector> in a cell never
got its <script> emitted. Now Page() emits it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two visual regressions from the custom-element port: 1. The played-row nested its dropdown menu (which contains <button> options) inside the toggle <button>. A <button> may not contain another <button>; the HTML parser force-closes the toggle on the nested button, and the source's explicit </div> tags then close ancestors early — ejecting the Purchases/Sessions/etc. sections out of the centered max-w container (they rendered full-width). Make the menu a sibling of the toggle, wrapped in a relative div so it still anchors under the toggle. 2. Both selector toggles dropped the original `flex flex-row gap-4 justify-between items-center` wrapper around their content, so the chevron stacked under the label (the GameStatus label is a display:flex block). Restore the wrapper — chevron sits on the right with proper spacing again. Verified by screenshot: sections back inside the centered container; both dropdowns render correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Selector menu options were bare <button>s with no padding, so the open dropdown items were cramped. Add a shared option class (block w-full text-left px-4 py-2 + hover), matching the original <a> list items. - The played-row's relative menu wrapper was a block div, so in the inline-flex button group the chevron toggle sat lower than the count button. Make the wrapper inline-flex and the group items-stretch so the two buttons align into one rounded group again. - Rebuild base.css for the newly-used utilities. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_GET_SESSION_COUNT_SCRIPT was a mark_safe string used as a child of the view_game content tree. Under the "only Safe nodes render unescaped" rule, a mark_safe *string* child is escaped — so the <script> showed as literal text on the page. Make it a Safe node (and drop the now-unused mark_safe import). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 13, 2026
…mental-unity Try unifying 3 different element interfaces
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/superpowers/plans/2026-06-13-html-js-authoring.md— replaces the trusted HTML/JS Python f-strings with three composing layers, then converts the three worst offenders.Layers
Element(additive, keepsMedia):Div(class_="x", hx_get="/y")[child1, child2]— kwargs attributes (class_→class,hx_get→hx-get,True→bare,False/None→omitted) and[]children. Still a walkable node tree.custom_element("tag", Props(...))emits a semantic tag; behavior lives ints/elements/<tag>.ts(customElements.define), wired by the nativeconnectedCallback(replaces theonSwapshim — fires on parse and htmx swap).TypedDictper element (register_element) →manage.py gen_element_typeswritests/generated/props.ts(interface + attribute reader). Renaming a prop failstsc.Toolchain
tscper-module (tsconfig.json) →games/static/js/dist/(build-only, gitignored).make ts/make ts-check(inmake check) /tsc --watchinmake dev.assetsstage (assets are built in the image, not committed).Conversions (Alpine retired for each)
GameStatusSelector→<game-status-selector>(was a ~70-line inline-Alpine f-string)SessionDeviceSelector→<session-device-selector>(deletes the shared Alpine dropdown helper)<play-event-row>(deletes the@@TOKEN@@template +.replace()hack)The two selectors share
ts/elements/dropdown.ts.Notable fix
SimpleTablestringified its cells, silently dropping each cell component's declaredMedia— so a<game-status-selector>in a table cell never got its<script>. It now collects row/header media and re-attaches it, soPage()emits it.Also
make checkis green end-to-end (separate commit).Testing
make checkgreen: ruff lint + format,tsc --noEmit(drift gate), 462 tests (unit + e2e). Newe2e/test_custom_elements_e2e.pydrives all three elements in real Chromium (open → select → PATCH/POST → DOM + DB updated).🤖 Generated with Claude Code