Multi-Document Upload with Split-Button Indicator

Date: 2026-03-09 Status: Approved Prototype: tmp-ui-sandbox/approach-A/button-indicator-v3.html

Problem

When users upload multiple documents of mixed types (invoices, contracts, notices), the current UI:

  1. Adds all files as "Processing" rows to the current page's table, even before classification determines their type
  2. Provides no feedback during upload + classification (empty 200 response for multi-file)
  3. Documents routed to other pages after classification just "disappear" without explanation

Design

Split Upload Button

The existing Upload button becomes a split button with two zones:

When idle, only the left side is visible (looks identical to the current Upload button). The right side slides in when a batch starts.

Popover

Anchored below the split button, right-aligned. Opens automatically on upload, toggleable via the status indicator.

Structure:

Batch separators appear when multiple uploads overlap (e.g., "Batch 1 — 11:42"). Newest batch prepends at the top.

Each file item is an <a> element linking to /documents/view/{id}:

Terminal states:

Behavior

Table Row Animation

When a document completes and belongs on the current page, a new row appears at the top of the table with a slide-in animation: translateY(-8px) + opacity 0→1, 0.3s ease-out. This animation applies to:

Backend Changes Required (HTMX-Idiomatic Revision)

  1. Upload endpoint response: Returns server-rendered HTML popover items (not JSON). The form uses hx-post with hx-swap="afterbegin" targeting #popover-body. Each file item is an <a> element with id="pop-file-{uuid}" and href="/documents/view/{uuid}". Skipped (duplicate) files are rendered as terminal state immediately.

  2. SSE events: No new event type needed. The existing new-document event carries HTML with multiple hx-swap-oob elements. HTMX processes all OOB elements automatically from a single event. Each event can include:

  3. No table row until completion: In-progress events are skipped (no DB query, no table row). Table rows only appear when ingestion completes AND the document type matches the current page. This fixes the "ghost row" problem where documents appeared on wrong-type pages.

  4. No SSE looper changes: The exec-fn continues returning a single {:event :data} map. The :data string simply contains concatenated HTML with OOB attributes — HTMX handles the rest.

What Doesn't Change

CSS Additions

New styles needed in style.css:

JS Additions

Minimal client-side state module (upload.js). The DOM is the source of truth: