Utilities

None of these show up on every page, but they decide whether the app feels finished: a print layout that actually produces a filled page instead of a blank one, an import dialog that never lies about a clean import, and a chat widget that survives being closed mid-typing.

ChatWidget

A floating conversation bubble, not a screen-centered overlay like Modal. Still a dialog: ESC and clicking outside the panel close it the same as other overlays, but it stays pinned to the corner so users can keep browsing the page while the chat is open. Enter sends a message, Shift+Enter inserts a new line.

Conversation widget

Same as Drawer and Toast, this component is pinned to the page's real viewport — look for its bubble in the bottom-right corner of the window, not inside this example box.

<UiChatWidget v-model:open="open" v-model:draft="draft" :messages="messages" :typing="typing" :unread="unread" @send="onSend" @retry="onRetry" />
PropTypeDefaultDescription
messagesChatMessage[][]Required. Conversation history, ordered oldest to newest.
titlestring'Support'Panel header title and the dialog's accessible name.
typingbooleanfalseShows a typing indicator with aria-live text.
unreadnumber0Badge on the floating button. Only visible while the panel is closed.
placeholderstring'Type a message…'Placeholder for the draft box.
agentNamestringundefinedName used by the avatar and the "typing" text.
agentAvatarstringundefinedURL of the agent's avatar photo.
v-model:openbooleanfalsePanel open/closed state.
v-model:draftstring''The text the user is currently typing.

Import dialog

Four steps — upload → preview → map → summary — over one parsing engine in utils/importer.ts. The parser is a character-by-character state machine, not split: real export files are full of quoted cells that contain the delimiter, and split corrupts them silently. The summary step below never lies about a clean import — it always shows how many rows failed and exactly why.

Upload, map, and see what would fail

<UiImportDialog v-model:open="open" :fields="fields" @import="onImport" />
PropTypeDefaultDescription
fieldsImportField[]Required. Destination columns: { key, label, required?, type? }.
openbooleanfalseTwo-way, written as v-model:open. Closing clears every derived step.
titlestring'Import data'Modal heading.
acceptstring'.csv,.txt'Forwarded to the file picker.
initialTextstringundefinedTesting/docs escape hatch only — skips File entirely.
@import(rows: Record<string, string>[]) => voidFires on the summary step. Carries ONLY the rows that passed validation.
@cancel() => voidFires when the dialog is dismissed without importing.

PrintLayout

A skeleton for print documents. assets/css/print.css hides the app chrome via visibility, not display: none on the parent — a display-none parent takes its children with it, and the app chrome's h-screen overflow-hidden pattern cuts the print document down to one blank page. That failure has already happened twice in ERPs using the same pattern; the :has() selector as a patch produced yet another blank page. PrintLayout only marks its root via data-print-root — the rest of the app chrome hides itself automatically, without touching each piece one by one.

A4/Letter ticket

Bus e-ticket

TKT-2026-00042

Route: Bandung — Jakarta

Departure 05:30 · Seat 12A · Passenger Budi Santoso

Aresa Executive fleet · Leuwipanjang pool

Printed via the operator panel, not an official proof of payment.

<UiPrintLayout paper="a4" title="Bus e-ticket" document-number="TKT-2026-00042">
  <template #header>Route: Bandung — Jakarta</template>
  <p>Departure 05:30 · Seat 12A · Passenger Budi Santoso</p>
  <template #footer>Printed via the operator panel, not an official proof of payment.</template>
</UiPrintLayout>

80mm thermal receipt

ARESA BUS

TKT-2026-00042

Bandung → Jakarta · 05:30

Seat 12A

<UiPrintLayout paper="thermal" :show-print-button="false">…</UiPrintLayout>

Props

PropTypeDefaultDescription
paper'a4' | 'letter' | 'thermal''a4'Print page size via a named @page — not size: auto.
titlestringundefinedDocument title, shown once at the top.
documentNumberstringundefinedDocument number, e.g. a ticket or invoice number.
showPrintButtonbooleantrueThe built-in "Print" button; always print-hidden.
repeatHeaderbooleanfalsePlaces the #header slot in the content table's <thead>, when the content is actually a table.

Print classes

PropTypeDefaultDescription
print-hiddenclassHide an element ONLY when printing (app chrome, action buttons).
print-onlyclassThe opposite: hidden on screen, shown only when printing.
print-break-beforeclassStart a new page before this element.
print-break-afterclassStart a new page after this element.
print-avoid-breakclassPrevent an element from being cut at a page boundary (e.g. one table row).