Commit Graph

10 Commits

Author SHA1 Message Date
lukas 82416e149d Convert onSwap widgets to custom elements (issue #18)
Replaces the four onSwap-based widgets with TypeScript custom elements
following the pattern from PR #16. Each widget gets a class extending
HTMLElement with connectedCallback/disconnectedCallback, typed props via
register_element + gen_element_types codegen, and lives in ts/elements/.

- range-slider: RangeSliderElement; Python uses _RangeSlider builder
- date-range-picker: DateRangePickerElement; Python uses _DateRangePicker builder
- search-select: SearchSelectElement; Python uses _SearchSelect builder;
  data-* attrs become plain attrs (data-name -> name, data-search-url -> search-url, etc.)
- filter-bar: FilterBarElement; props carry preset URLs; onclick/onsubmit
  attrs replaced with data-filter-bar-* sentinel attrs; all window.* globals removed

Deletes ts/range_slider.ts, ts/search_select.ts, ts/date_range_picker.ts,
ts/filter_bar.ts. Updates all tests and e2e pages to use the new element
selectors and script paths (dist/elements/<tag>.js).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 14:22:59 +02:00
lukas 19e9fd1419 Convert date_range_picker.js to TypeScript (issue #17)
- Add ts/date_range_picker.ts: typed port. CalendarState interface (with the
  dynamically-assigned refreshFromField) and an Anchor union replace the loose
  state object; date helpers and DOM queries fully typed; var → const/let
- Replace the DOMContentLoaded + per-element guard-flag + window global with
  onSwap("[data-date-range-picker]", ...), the documented init pattern — so the
  picker now also initializes inside htmx-swapped fragments. Drops the dead
  window.initDateRangePickers export
- Point the DateRangePicker component Media at dist/date_range_picker.js and load
  it as an ES module in the e2e page (was a deferred classic script)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:52:48 +02:00
lukas c809341064 Convert range_slider.js to TypeScript (issue #17)
- Add ts/range_slider.ts: typed port of the custom range-slider widget. Number
  inputs typed as HTMLInputElement; setTargetValue coerces via String(); mouse
  handlers typed MouseEvent; var → const/let
- Point the RangeSlider component Media and every e2e/test reference at the
  compiled dist/range_slider.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:48:36 +02:00
lukas cd17053860 Convert filter_bar.js to TypeScript (issue #17)
- Add ts/filter_bar.ts: typed port of the filter bar. Criterion / PillEntry /
  RangeField / DeselectableRadio interfaces replace the loose objects and the
  radio.wasChecked custom property; var → const/let throughout
- Window entry points (applyFilterBar/clearFilterBar/toggleStringFilterInput/
  showPresetNameInput/savePreset) declared in ts/globals.d.ts; readSearchSelect
  now called as window.readSearchSelect
- Drop the dead selectValue helper; factor the repeated path→mode mapping into
  presetMode()
- Point the FilterBar component Media and every e2e/test reference at the
  compiled dist/filter_bar.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:38:20 +02:00
lukas 541fb550ab Convert search_select.js to TypeScript (issue #17)
- Add ts/search_select.ts: typed port of the SearchSelect/FilterSelect widget.
  Exports SearchSelectOption / SearchSelectChangeDetail as the single source of
  truth for the "search-select:change" event contract
- add_purchase.ts now imports those types via `import type` (no runtime
  coupling), instead of redefining them locally
- Declare window.readSearchSelect in ts/globals.d.ts
- Point the SearchSelect component Media and every view/e2e/test reference at
  the compiled dist/search_select.js
- Update doc comments in common/components/search_select.py to name the TS source

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:34:06 +02:00
lukas 763c00c50e htpy-style sugar on Element: kwargs attributes + [] children 2026-06-13 21:03:57 +02:00
lukas 0c6c536d07 Ban SafeText-as-child: only Safe nodes render unescaped
Tightens the child model so the type is honest end to end. Previously a
``SafeText``/``mark_safe`` string passed as a child rendered unescaped — a
trusted-HTML-as-string backdoor that ``Child = Node | str`` couldn't express
(every ``SafeText`` is a ``str``). Now ``_child_key`` escapes *every* string
child; the only way to put trusted pre-rendered HTML into the tree is a
``Safe`` node. So a ``str`` child is always untrusted text — which is exactly
what the renderer escapes.

Converted the trusted-HTML children that relied on the old passthrough:

- ``CsrfInput`` and the Alpine selectors (``GameStatusSelector`` /
  ``SessionDeviceSelector``) now return ``Safe`` nodes instead of ``mark_safe``
  strings — they are always tree children.
- ``popover_content`` is now a ``Child`` (it is rendered as a child); the one
  HTML caller (``LinkedPurchase``) passes ``Safe(...)``.
- View-side children that were ``mark_safe`` strings → ``Safe(...)``:
  ``_played_row`` (game detail), the stat SVGs and ``&nbsp;`` spacer (game),
  the login table (auth), the manual session-form field/label markup
  (session), and ``_purchase_name`` (stats).
- ``SimpleTable.header_action`` typed ``Child``.

The script-tag string helpers (``ModuleScript`` / ``StaticScript`` /
``ExternalScript``) stay ``SafeText`` strings: they are only ever joined into
the ``scripts=`` string, never used as tree children.

``Children`` regains a bare ``Node`` member (a single node child is valid);
the one ``*children`` site (``Popover``) normalises via ``as_children`` first.
Tests that asserted the old SafeText-passthrough now assert the new rule
(mark_safe child escaped; ``Safe`` node passes through). Full suite green
(445; +2 new escaping tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:35:43 +02:00
lukas 9c42d85f52 Migrate remaining Component() callers to Element; delete the shim
The legacy back-compat ``Component(tag_name=...)`` function (a thin
string-returning wrapper over ``Element``) was the last piece of the
pre-node-tree API. Migrate its ~18 call sites across the views to the node
builders and remove it:

- stats_content.py: the table helpers now use the whitelisted ``Td`` / ``Th``
  / ``Tr`` builders and ``Element`` for table/tbody/thead/h1; helper return
  types are ``Node``.
- auth.py / statuschange.py / game.py / purchase.py: the hand-built
  ``<form>`` / ``<button>`` / ``<h1>`` / ``<h2>`` / ``<table>`` markup now uses
  ``Element("tag", ...)``.
- core.py: drop the ``Component()`` function and its back-compat note;
  ``common/components/__init__`` no longer exports it.
- Tests that exercised the shim now target ``Element`` directly
  (test_components cache/escaping/edge-case classes; test_node_tree drops the
  legacy-parity and legacy-bridge cases, which ``Element`` coverage subsumes).
- CLAUDE.md: drop the "legacy Component retained for back-compat" notes.

Full suite green (443; one obsolete legacy-bridge test removed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:51:27 +02:00
Claude 0819ddb87d Phase 3: declare component media that bubbles through the node tree
The JS-bearing widgets now declare their script dependencies, so a view
no longer needs to know which scripts a component requires:

- SearchSelect / FilterSelect → search_select.js
- RangeSlider → range_slider.js
- DateRangePicker → date_range_picker.js
- YearPicker → datepicker.umd.js (external, from Phase 2)
- FilterBar chrome → filter_bar.js

Because the filter-bar internals now build a node tree (the legacy
Component() string-builder calls became Element/Div), each bar's
collect_media() returns its own filter_bar.js merged with the scripts
that bubble up from the FilterSelect / RangeSlider / DateRangePicker
widgets it contains — exactly the set the views thread by hand today.

Adds Node.with_media() so a function-built node can declare media
without a full BaseComponent subclass, and tests proving the bubbling.

Note: the six *FilterBar functions still share the _filter_bar chrome
helper rather than a BaseComponent class hierarchy; folding them into
one is a follow-up that does not affect media collection (Phase 4).

https://claude.ai/code/session_01BKurBhE3Qj25p7Bfsg7EeK
2026-06-13 07:24:29 +00:00
Claude f673f3ac80 Phase 1: add lazy node tree (Node/Element/Safe/Fragment/Media)
Introduce a FastHTML-style component model alongside the existing
function-based one, purely additive:

- Node: base renderable; __html__/__str__ render lazily so str()/f-string
  composition keeps working during migration.
- Element: the single class for any HTML element (tag + attrs + children),
  rendering via the existing memoized _render_element.
- Safe: wraps pre-rendered HTML (migration bridge for f-string components).
- Fragment: ordered children with no wrapper tag (replaces str(a)+str(b)).
- BaseComponent: base for higher-level components; render() returns a
  subtree, media declared via a Media attribute.
- Media: declarative JS deps with order-preserving dedup merge.
- collect_media()/render() helpers walk the tree.

The legacy Component() function now builds an Element and is Node-aware in
its child handling, so a tree mixing string- and node-returning components
renders correctly with byte-identical output. No call sites changed yet.

https://claude.ai/code/session_01BKurBhE3Qj25p7Bfsg7EeK
2026-06-13 06:56:37 +00:00