List region

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.

Name▲OwnerStatus
Capacity reviewJ. DiasActive
Load studyK. NovakDraft
Quarterly auditM. ReyesActive
Site walkdownM. ReyesClosed
Vendor renewalA. OseiDraft

Showing 5 of 5

Copy this

html
<!-- 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&amp;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&amp;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&amp;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.

RequestTriggerResponse fragmentSwapEnvelopeStates
GET /mock/list-regionclick on a sort headerFull list-region outerHTML reordered by ?sort=&dir= (leftover-honest include_closed / as_of ride); active column caret ▲/▼closest [data-list-region] outerHTMLouterpopulated
GET sample-list-export.csvclick Export CSV (via dz.downloadCsv)text/csv file body (download, not a DOM swap)n/a — Blob downloadnonepopulated 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=outer
  • GET 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 / nested data-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’s hx-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

html
<!-- 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

html
<!-- 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

text
// 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

text
<!-- 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).

NodeAttrConstraint
[data-list-region]data-list-regionpresent (any value)

Ingestion model ListRegion

Server-side shape before render — one normalisation boundary for producers.

FieldTypeRequired
body_htmlstringoptional

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.

python
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).

NameOwner
Quarterly auditM. Reyes

Showing 1 of 14

Notes

Dual-lock root is 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