Date: 2026-03-09
Status: Approved
Prototype: tmp-ui-sandbox/approach-A/button-indicator-v3.html
When users upload multiple documents of mixed types (invoices, contracts, notices), the current UI:
The existing Upload button becomes a split button with two zones:
Upload button with upload icon. Clicking opens the file picker. Works identically whether idle or mid-batch.When idle, only the left side is visible (looks identical to the current Upload button). The right side slides in when a batch starts.
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:
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:
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.
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:
hx-swap-oob="outerHTML" targeting #pop-file-{id}) — silently no-ops if no matching element exists (e.g., email-ingested documents)hx-swap-oob="true" for in-place updates)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.
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.
<input multiple>, same max 5 limit, HTMX hx-encoding="multipart/form-data")sse.clj)New styles needed in style.css:
.split-btn, .split-btn-upload, .split-btn-status — split button layout.upload-popover, .popover-header, .popover-body, .popover-summary — popover structure.pop-file, .pop-file-icon, .pop-file-info, .pop-file-type — file items.pop-activity, .pop-activity-bar — indeterminate activity animation.batch-separator — batch grouping in popover.row-new with row-slide-in keyframes — table row entrance animation.popover-backdrop — click-outside dismissalMinimal client-side state module (upload.js). The DOM is the source of truth:
onUploadResponse() — called via hx-on::after-request after upload; shows status indicator, opens popoveronItemLoad() — called via hx-on::load when SSE replaces a popover item; recounts badge, updates summaryupdateBadge() — queries #popover-body .pop-file.processing countupdateSummary() — queries completed/failed/skipped countstogglePopover() / openPopover() / closePopover() — visibility toggles