List region data SpriteEndpoint
The in-card data table: CSV export, sortable headers, a scrollable table body, and an overflow count.
Layer: L1 surface · Recipe: unset — see docs/agent/pick-a-surface.md. Curriculum: AGENTS.md; pick matrix: docs/agent/pick-a-surface.md; blast radius: CONSUMER_MAP.md.
Copy this
<!-- icons: include the icon sheet once per page (see the Setup section, #setup) -->
<div class="list-region" data-list-region id="hm-list-region-demo">
<div class="list-actions">
<div class="list-action-group"><button type="button" class="list-csv-button" title="Export CSV" aria-label="Export CSV" data-csv-endpoint="sample-list-export.csv" data-csv-filename="work-items.csv" onclick="window.dz.downloadCsv(this.dataset.dzCsvEndpoint||this.dataset.csvEndpoint, this.dataset.dzCsvFilename||this.dataset.csvFilename)"><svg class="icon" aria-hidden="true"><use href="#i-download"/></svg></button></div>
</div>
<div class="list-scroll">
<table class="list-table">
<thead>
<tr>
<th><a class="list-sort-link" hx-get="/mock/list-region?sort=name&dir=desc" hx-target="closest [data-list-region]" hx-swap="outerHTML">Name<span>▲</span></a></th>
<th><a class="list-sort-link" hx-get="/mock/list-region?sort=owner&dir=asc" hx-target="closest [data-list-region]" hx-swap="outerHTML">Owner</a></th>
<th><a class="list-sort-link" hx-get="/mock/list-region?sort=status&dir=asc" hx-target="closest [data-list-region]" hx-swap="outerHTML">Status</a></th>
</tr>
</thead>
<tbody>
<tr class="list-row is-clickable">
<td>Capacity review</td>
<td>J. Dias</td>
<td>Active</td>
</tr>
<tr class="list-row is-clickable">
<td>Load study</td>
<td>K. Novak</td>
<td>Draft</td>
</tr>
<tr class="list-row is-clickable">
<td>Quarterly audit</td>
<td>M. Reyes</td>
<td>Active</td>
</tr>
<tr class="list-row ">
<td>Site walkdown</td>
<td>M. Reyes</td>
<td>Closed</td>
</tr>
<tr class="list-row ">
<td>Vendor renewal</td>
<td>A. Osei</td>
<td>Draft</td>
</tr>
</tbody>
</table>
</div>
<p class="list-overflow">Showing 5 of 5</p>
</div>Server exchange
When the client affordance finishes (click, confirm, keystroke…), htmx issues this request. Your API must return the response fragment in the table — usually HTML, not JSON (unless the partial says otherwise). Dazzle often renders these routes from the app model; a standalone HTMX4 app implements them explicitly. The Envelope column is the exchange envelope (part of the Swap contract) — what the response may re-emit relative to the persistent slot.
Do not reimplement the gallery. Flash toasts (e.g. “Deleted (demo).”), /mock/* paths, and other static-site scaffolding are demo-only (MOCK_HTMX in site/build_site.py). They are not Hyperpart surface and not a product API. If an agent is stuck “making the toast work,” stop — implement the exchange row below instead.
| Request | Trigger | Response fragment | Swap | Envelope | States |
|---|---|---|---|---|---|
GET /mock/list-region | click on a sort header | Full list-region outerHTML reordered by ?sort=&dir= (leftover-honest include_closed / as_of ride); active column caret ▲/▼ | closest [data-list-region] outerHTML | outer | populated |
GET sample-list-export.csv | click Export CSV (via dz.downloadCsv) | text/csv file body (download, not a DOM swap) | n/a — Blob download | none | populated error |
Swap contract
Agent-visible HTMX topology (ADR-0054 / decision 0012). exchange envelope = what the response may re-emit relative to the persistent slot (body_only | outer | none | host_owned | document). dual-lock validates part markup only — not this envelope. Stem: stems/morph-safe-hypermedia.md; decision: docs/decisions/0012-swap-identity-contract.md.
Gallery mocks may approximate morph with innerHTML — production follows the Swap + Envelope columns in Server exchange.
Exchanges (swap · envelope)
GET /mock/list-region→ closest [data-dz-list-region] outerHTML · envelope=outerGET sample-list-export.csv→ n/a — Blob download · envelope=none
Envelope rules
body_only— innerHTML / innerMorph into a slot; response is interior only (no re-wrap of slot id / nesteddata-dz-region).outer— outerHTML / outerMorph; response may carry identity.none— no HTML swap (JSON/204/bytes; client or OOB companion).host_owned— swap target/mode chosen by the host button’shx-target/hx-swap.document— full navigation / document load (not a fragment).- Slot owns stable
id/ domain keys; state in DOM, not Alpine.
Envelope response examples
What the server returns for each exchange on List region. Match the exchange envelope; dual-lock still applies to interior markup.
GET /mock/list-region · envelope=outer
Correct response for outer (outerHTML / outerMorph) replacing #slot.
Do — correct response body
<!-- envelope=outer → response root may carry the slot identity -->
<div id="slot" class="dz-card" data-dz-card>
<!-- full replacement of the previous element -->
</div>
Don’t — violates outer
<!-- WRONG for outer: body-only fragment when the host expects a full element -->
<!-- (missing root that matches hx-target — leaves empty or nested junk) -->
<span>partial content without the target root</span>
GET sample-list-export.csv · envelope=none
Correct response for none (raw fetch / no HTML swap).
Do — correct response body
// envelope=none — no HTML swap (JSON/204; client or OOB companion)
// HTTP 204 No Content
// or:
{ "ok": true }
// Application/json; status 200
// Optional: separate OOB HTML fragments if the host declares them
Don’t — violates none
<!-- WRONG: HTML body when hx-swap is none / raw fetch expects JSON|204 -->
<div class="dz-alert">Deleted</div>
How to use it
No extended guidance authored yet — start from Copy this and the dependency chips (Primitive = markup only; controller = load listed JS; Endpoint = implement Server exchange).
Seams
- copy the partial under Copy this; keep root class and data-* modifiers so the CSS/JS bundle matches
- implement Server exchange endpoints; return HTML fragments, not JSON
- satisfy the DOM contract tables (CI stop-ship)
DOM contract
What the emitted HTML must satisfy — the table is the required surface; Python under contracts/ is the package-internal dual-lock CI runs (tests/test_contracts.py), not an app route. Standalone HTMX4: implement the API so responses match this markup. Dazzle: the agent emits SSR that already satisfies it. Do not invent attrs outside these tables. For request/response wiring see Server exchange.
contracts/list_region.py
Required in the DOM: root [data-list-region] (part list-region). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).
| Node | Attr | Constraint |
|---|---|---|
[data-list-region] | data-list-region | present (any value) |
Ingestion model ListRegion
Server-side shape before render — one normalisation boundary for producers.
| Field | Type | Required |
|---|---|---|
body_html | string | optional |
Exemplar render()
Executable in CI: the Python below is render(); the boxed preview is render(EXEMPLARS[0]) — the first fixture the dual-lock tests emit, not a separate widget and not gallery mock data.
def render(lr: ListRegion) -> str:
"""Model → list-region root wrapper."""
return f'<div class="dz-list-region" data-dz-list-region>{lr.body_html}</div>'
Live output of render(EXEMPLARS[0]) — fixture markup the dual-lock validates (sample field values only).
| Name | Owner |
|---|---|
| Quarterly audit | M. Reyes |
Showing 1 of 14
Notes
data-list-region (contracts/list_region.py). CSV export is wired with data-csv-endpoint + data-csv-filename and window.dz.downloadCsv (gallery serves sample-list-export.csv as the downloadable artifact). Leftover-honest include_closed / as_of ride data-csv-endpoint (cycle 2174); the bare path invented open-only / current CSV. Sortable headers are list-sort-link anchors with hx-get ?sort=&dir= — leftover-honest include_closed / as_of ride the hx-get (cycle 2172); dropping them invents open-only / current after a sort click. Rest-state gallery omits them (oral #33). The host re-renders the region; the active column shows a caret. Rows with a drill URL carry is-clickable. For selection/filters/pagination use the grid Hyperpart.Source files
Canonical registration in the registry. No dedicated controller — CSS for this part lives in the layered bundle.
site/registry.py · contracts/list_region.py