live_interaction_contracts

This is not a component library. It is a set of executable interaction contracts for Phoenix LiveView and browser-native state machines, plus a conformance harness that continuously verifies them against real browsers.

Its primary artifacts are a conformance harness and a delegation ledger — not components. Components (a popover, a combobox) are derivations of the contracts, not the point.

What it answers

For any browser-native interaction machine (popover, dialog, focus, scroll, a combobox's listbox), it answers, with executable evidence across Chromium, Firefox, and WebKit:

Why it exists

In a LiveView app, interaction state and server-rendered content share one DOM subtree with two writers separated by network latency. The hard case — a combobox: server-rendered results + client-owned active descendant + versioned async query + browser-owned listbox shell — is exactly where the naive approaches fail. This project establishes, by test, which delegations are safe and which are not, and freezes that as a contract a conforming implementation must uphold.

v1 frozen status

Frozen for v1: the kernel invariants, the delegation-ledger format, the conformance harness, CI-as-fuse, the popover / tooltip / menu / select / combobox / cursor / channel contracts, the shipped LiveInteractionContracts.Channel guard, anchored placement presets, and the amber dialog-adapter contract (CONTRACT_DIALOG.md, 7/7 conformance-gated). Still not frozen: a polished public <.dialog> component and any real-device IME claim. See SPEC.md §"v1 verdict".

Repository map

PathRole
SPEC.mdDurable project statements + v1 frozen/not-frozen verdict
KERNEL.mdThe five kernel members, precise frozen definitions
DELEGATION_LEDGER.mdLedger format + classification rules (durable)
delegation-ledger.md / .jsonThe current ledger — generated by the harness, not hand-edited
CONFORMANCE.mdThe harness, the two-tier CI gate, how the ledger is regenerated
CURSOR_CONTRACT.mdClient-owned coordination contract
CHANNEL_CONTRACT.mdVersioned async query/result contract
REFERENCE_POPOVER_CONTRACT.mdFirst green reference machine
CONTRACT_DIALOG.mdAmber reference adapter (repairs red native <dialog>)
CONTRACT_MENU.mdMenu reference (green + cursor): popover + aria-activedescendant
CONTRACT_ISLAND.mdClient-built children need phx-update=ignore (proven)
CONTRACT_TOOLTIP.mdTooltip reference (green, pure delegation): hover/focus popover
CONTRACT_SELECT.mdSelect reference (green): popover + cursor + server-owned value
CONTRACT_COMBOBOX.mdCombobox reference (green w/ construction requirement): popover × cursor × Channel
RED_FIXTURES.mddialog + details: why they are excluded from public API
BACKLOG.mdAmber/unknown items and deferred work
TASKS.mdRoadmap to v1.0 — milestones with acceptance gates
REPRODUCE.mdExact commands to reproduce every spike
BUILDING_COMPONENTS.mdPlaybook for adding the next conformance-backed component
mix.exs, lib/The Mix package + conformance tasks
lib/live_interaction_contracts/components/popover.exReference <.popover> component (HEEx anatomy)
lib/live_interaction_contracts/components/menu.exReference <.menu> component (popover + cursor)
lib/live_interaction_contracts/components/tooltip.exReference <.tooltip> component (hover/focus popover)
lib/live_interaction_contracts/components/select.exReference <.select> component (popover + cursor + server-owned value)
lib/live_interaction_contracts/components/combobox.exReference <.combobox> component (popover × cursor × Channel)
lib/live_interaction_contracts/components.exSingle use LiveInteractionContracts.Components import entrypoint
lib/live_interaction_contracts/channel.exChannel — the server-side monotonic guard as library code
(colocated hooks)Client JS ships inside each component as a colocated hook (LiveView 1.1+)
priv/harness/Multi-version conformance harness (target app + driver + ledger gen)
priv/cursor-spike/Client-owned tier evidence (replace-with-same-id)
priv/combobox-spike/Composition evidence (Delegation × Cursor × Channel)
priv/dialog-adapter-spike/Amber dialog-adapter evidence (7/7 conformance)
priv/island-survival-spike/Island survival: client-built children vs patches
priv/popover-reference/Reference-component conformance (7/7, dogfoods <.popover>)
priv/menu-reference/Menu conformance (8/8, dogfoods <.menu>)
priv/tooltip-reference/Tooltip conformance (8/8, dogfoods <.tooltip>)
priv/select-reference/Select conformance (9/9, dogfoods <.select>)
priv/combobox-reference/Combobox conformance (9/9, dogfoods <.combobox> + Channel)
reports/Dated evidence reports (version-specific; not durable spec; repo-only, not packaged)
.github/workflows/conformance.ymlThe fuse
docs_site/Minimal docs page (components + live ledger); elixir docs_site/build.exs regenerates

Install

# harness / ledger / fuse only (dev/test):
{:live_interaction_contracts, "~> 0.1", only: [:dev, :test], runtime: false}
# also using the reference components (e.g. <.popover>) at runtime:
{:live_interaction_contracts, "~> 0.1"}

The conformance tasks require elixir, node, and Playwright browsers (npx playwright install). The reference components require phoenix_live_view 1.1+ (already in your app). Their client JS ships as colocated hooks — no manual registration, no vendored JS file. mix compile extracts them; then in your app.js:

import {hooks as licHooks} from "phoenix-colocated/live_interaction_contracts";
new LiveSocket("/live", Socket, {hooks: {...hooks, ...licHooks}});

Minimal consumer example (your own mix.exs must include the peer deps):

# mix.exs deps — phoenix_live_view is YOUR dependency, not transitively provided
{:phoenix, "~> 1.8"},
{:phoenix_live_view, "~> 1.1"},
{:live_interaction_contracts, "~> 0.1"}
defmodule MyAppWeb.Demo do
use Phoenix.Component
use LiveInteractionContracts.Components
def demo(assigns) do
~H"""
<.popover id="demo" placement="bottom">
<:trigger>Open</:trigger>
<:content>Patch-safe by contract.</:content>
</.popover>
"""
end
end

The conformance tasks below run from your project — they boot the packaged fixtures against your LiveView version (Playwright must be installed: cd deps/live_interaction_contracts && npm i).

Quickstart

# runs the harness in real browsers against your project's LiveView version,
# regenerates the ledger, and asserts the fuse (non-zero exit on regression)
mix live_interaction_contracts.test # all three browsers
mix live_interaction_contracts.test --browser chromium
mix live_interaction_contracts.test --suite combobox
mix live_interaction_contracts.test --lv "github:phoenixframework/phoenix_live_view#main"
mix live_interaction_contracts.ledger # regenerate ledger only
# audit a target app for patch-safety / dual-write risk (advisory, KERNEL rules)
mix live_interaction_contracts.audit --path /path/to/app
# version-upgrade fuse: diff two harness result sets, non-zero on drift
mix live_interaction_contracts.compare --a before.json --b after.json

By default the harness detects and tests your project's LiveView version. See REPRODUCE.md for the raw (non-mix) invocation used to develop the spikes.

Reading order

To add a component, follow BUILDING_COMPONENTS.md — a mechanical, copy-the- popover-reference playbook that encodes every boot/Playwright/morphdom gotcha so the work is delegatable.

SPEC.mdKERNEL.md → the individual contracts → CONFORMANCE.md. The reports/*_REPORT.md files are the evidence behind the contracts; read them when you doubt a claim. They are version-specific observations, deliberately kept out of the durable spec.