@@ -471,14 +471,15 @@ function renderSimulateBody(): string {
471471 x-on:dragleave.window.prevent.stop="onDragLeave($event)"
472472 x-on:drop.window.prevent.stop="onDrop($event)">
473473 ${ renderHero ( ) }
474+ ${ renderDropBar ( ) }
475+ ${ renderQueueCard ( ) }
474476 <div class="grid grid-cols-[minmax(0,1fr)_360px] gap-[22px] items-start">
475477 ${ renderEditorCard ( ) }
476478 <div class="flex flex-col gap-4">
477479 ${ renderTweaksCard ( ) }
478480 ${ renderSendCard ( ) }
479481 </div>
480482 </div>
481- ${ renderQueueCard ( ) }
482483 ${ renderDropOverlay ( ) }
483484 </div>
484485 ${ renderSimulateScript ( ) }
@@ -517,7 +518,7 @@ function renderDropOverlay(): string {
517518 */
518519function renderQueueCard ( ) : string {
519520 return `
520- <div x-show="queue.length > 0" x-cloak class="mt-5 ">
521+ <div x-show="queue.length > 0" x-cloak class="mb-[22px] ">
521522 <div class="card flex flex-col overflow-hidden">
522523 <div class="flex items-center gap-3 py-3 px-[18px] border-b border-line bg-paper-2">
523524 <span class="font-mono text-[11.5px] text-ink-2 font-medium">queue.batch</span>
@@ -556,13 +557,14 @@ function renderQueueCard(): string {
556557 x-text="item.name"></div>
557558 <div class="font-mono text-[11px] text-ink-3 truncate" x-text="item.preview"></div>
558559 </div>
559- <!-- Live-progression chip: distinct tones per lifecycle state so the
560- user can watch each message traverse sending → waiting → verdict. -->
560+ <!-- Mirrors Inbound Messages chips: chip-ok/processed,
561+ chip-warn/needs mapping, chip-err/error. In-flight states use
562+ the plain chip + spinner — same treatment Inbound gives the
563+ 'processing' state for received-but-unprocessed messages. -->
561564 <span class="chip text-[10.5px] shrink-0 flex items-center gap-1"
562565 :class="{
563- 'chip-accent': item.status === 'sending' || item.status === 'waiting',
564566 'chip-ok': item.status === 'sent',
565- 'chip-warn': item.status === 'held' || item.status === 'pending-verdict' ,
567+ 'chip-warn': item.status === 'held',
566568 'chip-err': item.status === 'error',
567569 }">
568570 <template x-if="item.status === 'sending' || item.status === 'waiting'">
@@ -588,66 +590,71 @@ function renderQueueCard(): string {
588590
589591function renderHero ( ) : string {
590592 return `
591- <div>
593+ <div class="mb-[22px]" >
592594 <div class="text-[11px] tracking-[0.1em] uppercase text-ink-3 font-medium">Compose & send · MLLP</div>
593- <div class="flex items-end gap-4 mt-1.5">
594- <div class="flex-1">
595- <h1 class="h1">Simulate Sender</h1>
596- <div class="mt-1.5 text-[13.5px] text-ink-2">Pick a message type, tweak the text, fire it at the listener. Pairs with Inbound to show the whole loop.</div>
595+ <h1 class="h1 mt-1.5">Simulate Sender</h1>
596+ <div class="mt-1.5 text-[13.5px] text-ink-2">Pick a message type, tweak the text, fire it at the listener. Pairs with Inbound to show the whole loop.</div>
597+ </div>
598+ ` ;
599+ }
600+
601+ /**
602+ * Slim full-width upload affordance. Sits between the hero and the
603+ * editor/queue: things that act on a *batch* (this drop bar, the queue card)
604+ * stack above the editor; the right column stays focused on single-message
605+ * tooling (sample picker, send button). Discovery only — the actual drop
606+ * works anywhere on the page; clicking the bar opens the native file picker
607+ * for keyboard/a11y paths. Tone flips to accent while a drag is active.
608+ */
609+ function renderDropBar ( ) : string {
610+ return `
611+ <label class="cursor-pointer select-none rounded-[7px] border border-dashed px-4 py-2.5 mb-[22px] flex items-center gap-3 transition-colors"
612+ :class="dragDepth > 0 ? 'border-accent bg-accent-soft' : 'border-line hover:border-ink-3 bg-paper-2'">
613+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
614+ stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"
615+ class="shrink-0"
616+ :class="dragDepth > 0 ? 'text-accent-ink' : 'text-ink-3'">
617+ <path d="M12 3v12"/>
618+ <path d="m7 8 5-5 5 5"/>
619+ <path d="M5 21h14"/>
620+ </svg>
621+ <div class="flex-1 leading-[1.35] min-w-0">
622+ <div class="text-[12.5px] font-medium" :class="dragDepth > 0 ? 'text-accent-ink' : 'text-ink'">
623+ <span x-show="dragDepth === 0">Drop files, folders, or .zip — send many at once</span>
624+ <span x-show="dragDepth > 0" x-cloak>Release to queue</span>
625+ </div>
626+ <div class="text-[10.5px]" :class="dragDepth > 0 ? 'text-accent-ink' : 'text-ink-3'">
627+ <span x-show="dragDepth === 0">or click to browse</span>
628+ <span x-show="dragDepth > 0" x-cloak>…queuing</span>
597629 </div>
598- <!-- Drop-zone affordance. Purely discovery — the actual drop works
599- anywhere on the page (outer x-data listens for dragover/drop),
600- but a visible "you can drop things" chip keeps the feature from
601- being invisible. Click opens the native file picker for keyboard
602- / accessibility paths. The tone flips to accent while dragging. -->
603- <label class="shrink-0 mb-[6px] cursor-pointer select-none rounded-[7px] border border-dashed px-3.5 py-2.5 flex items-center gap-2.5 transition-colors"
604- :class="dragDepth > 0 ? 'border-accent bg-accent-soft' : 'border-line hover:border-ink-3 bg-paper-2'">
605- <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
606- stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"
607- class="shrink-0"
608- :class="dragDepth > 0 ? 'text-accent-ink' : 'text-ink-3'">
609- <path d="M12 3v12"/>
610- <path d="m7 8 5-5 5 5"/>
611- <path d="M5 21h14"/>
612- </svg>
613- <div class="leading-[1.35]">
614- <div class="text-[12.5px] font-medium" :class="dragDepth > 0 ? 'text-accent-ink' : 'text-ink'">
615- <span x-show="dragDepth === 0">Drop files, folders, or .zip</span>
616- <span x-show="dragDepth > 0" x-cloak>Release to queue</span>
617- </div>
618- <div class="text-[10.5px]" :class="dragDepth > 0 ? 'text-accent-ink' : 'text-ink-3'">
619- <span x-show="dragDepth === 0">send many at once</span>
620- <span x-show="dragDepth > 0" x-cloak>…queuing</span>
621- </div>
622- </div>
623- <input type="file" multiple accept=".hl7,.txt,.zip,application/zip"
624- class="sr-only"
625- x-on:change="onFilePicker($event)"/>
626- </label>
627630 </div>
628- <div class="mb-[22px]"></div>
629- </div>
631+ <input type="file" multiple accept=".hl7,.txt,.zip,application/zip"
632+ class="sr-only"
633+ x-on:change="onFilePicker($event)"/>
634+ </label>
630635 ` ;
631636}
632637
633638function renderEditorCard ( ) : string {
639+ // Textarea goes read-only whenever a batch is queued. Edits made in queue
640+ // mode never sync back to the queue row, so allowing them silently loses
641+ // the user's changes — readonly makes the constraint explicit.
634642 return `
635643 <div class="card flex flex-col overflow-hidden">
636644 <div class="flex items-center gap-2.5 py-3 px-[18px] border-b border-line bg-paper-2">
637645 <span class="font-mono text-[11.5px] text-ink-2 font-medium truncate" x-text="activeFileName" :title="activeFileName"></span>
638646 <span class="chip text-[10.5px] shrink-0" x-show="hl7Version" x-cloak>HL7v2 · <span x-text="hl7Version"></span></span>
639647 <span class="chip text-[10.5px] shrink-0" x-text="segmentCount + ' segments'"></span>
648+ <span class="chip text-[10.5px] shrink-0" x-show="queue.length > 0" x-cloak>read-only · queued</span>
640649 </div>
641650 <textarea
642- class="font-mono clean-scroll px-[22px] py-5 text-[13px] leading-[1.7] border-none outline-none bg-surface text-ink min-h-[360px] resize-y w-full"
651+ class="font-mono clean-scroll px-[22px] py-5 text-[13px] leading-[1.7] border-none outline-none min-h-[360px] resize-y w-full"
652+ :class="queue.length > 0 ? 'bg-paper-2 text-ink-2 cursor-default' : 'bg-surface text-ink'"
643653 x-ref="editor"
644654 x-model="raw"
655+ :readonly="queue.length > 0"
645656 spellcheck="false"
646657 ></textarea>
647- <div class="flex items-center gap-3.5 py-2.5 px-[18px] border-t border-line bg-paper-2 text-[11.5px] text-ink-3 font-mono">
648- <span>pipe-delimited · CR or LF endings ok</span>
649- <span class="ml-auto"><span x-text="raw.length"></span> chars · <span x-text="segmentCount"></span> segments</span>
650- </div>
651658 </div>
652659 ` ;
653660}
@@ -1078,16 +1085,18 @@ function renderSimulateScript(): string {
10781085 },
10791086
10801087 queueChipLabel(item) {
1081- // Queue rows surface the full live send lifecycle so the user
1082- // can watch each message progress through MLLP → processor verdict.
1083- // Matches the Send card's steps but condensed into a chip label.
1088+ // Vocabulary mirrors the Inbound Messages list so a queued row's
1089+ // verdict reads identically to what the user will see on /incoming-messages:
1090+ // processed / needs mapping / error. In-flight states use a plain
1091+ // 'processing' chip + spinner — same treatment Inbound gives the
1092+ // received-but-unprocessed window.
10841093 if (item.status === 'sending') return 'sending…';
1085- if (item.status === 'waiting') return 'waiting for processor… ';
1086- if (item.status === 'pending-verdict') return 'MLLP sent · no verdict';
1087- if (item.status === 'sent') return 'sent ';
1088- if (item.status === 'held') return 'held for mapping';
1094+ if (item.status === 'waiting') return 'processing ';
1095+ if (item.status === 'pending-verdict') return 'no verdict yet ';
1096+ if (item.status === 'sent') return 'processed ';
1097+ if (item.status === 'held') return 'needs mapping';
10891098 if (item.status === 'error') return 'error';
1090- return 'pending ';
1099+ return 'queued ';
10911100 },
10921101
10931102 get queueProgress() {
0 commit comments