thermoprint 0.3.0

A fluent ESC/POS receipt builder for thermal printers. Targets native (Tauri/desktop) and WASM/npm.
Documentation
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>thermoprint — Receipt Builder</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div class="app">
    <!-- ── Sidebar: Receipt Builder ── -->
    <aside class="sidebar">
      <div class="sidebar-header">
        <div class="logo-row">
          <svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
            <rect x="3" y="7" width="16" height="10" rx="2" fill="#1e293b"/>
            <rect x="3" y="7" width="16" height="4" rx="2" fill="#e94560"/>
            <rect x="6" y="12" width="10" height="1.2" rx="0.6" fill="#334155"/>
            <rect x="7" y="14" width="8" height="5" rx="1" fill="#f1f5f9"/>
            <rect x="8.5" y="15.5" width="1" height="2.5" fill="#334155"/>
            <rect x="10" y="15.5" width="1.5" height="2.5" fill="#334155"/>
            <rect x="12" y="15.5" width="1" height="2.5" fill="#334155"/>
            <circle cx="17" cy="9" r="1" fill="#4ecca3"/>
          </svg>
          <span class="logo-text">thermo<span class="accent">print</span></span>
        </div>
        <p class="subtitle">ESC/POS Receipt Builder</p>
      </div>

      <form id="receipt-form" autocomplete="off">
        <!-- Shop -->
        <section class="form-section">
          <h3 class="section-title">Shop Info</h3>
          <div class="field">
            <label>Shop Name</label>
            <div class="input-with-align">
              <input title="shop-name" type="text" id="shop-name" value="MA BOUTIQUE" />
              <div class="align-btns" data-target="header-align">
                <button type="button" class="align-btn active" data-align="center" title="Center">
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="10" x2="6" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="18" y1="18" x2="6" y2="18"/></svg>
                </button>
                <button type="button" class="align-btn" data-align="left" title="Left">
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>
                </button>
                <button type="button" class="align-btn" data-align="right" title="Right">
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="21" y1="10" x2="7" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="7" y2="18"/></svg>
                </button>
              </div>
            </div>
          </div>
          <div class="field">
            <label>Phone</label>
            <input title="shop-phone" type="text" id="shop-phone" value="+221 77 000 00 00" />
          </div>
          <div class="field">
            <label>Address</label>
            <input title="shop-address" type="text" id="shop-address" value="Dakar, Senegal" />
          </div>
          <div class="field-row">
            <div class="field">
              <label>Currency</label>
              <input title="currency" type="text" id="currency" value="FCFA" maxlength="8" />
            </div>
            <div class="field">
              <label>Paper Width</label>
              <select title="paper-width" id="paper-width">
                <option value="58mm">58 mm</option>
                <option value="80mm" selected>80 mm</option>
                <option value="a4">A4</option>
              </select>
            </div>
          </div>
          <div class="field">
            <label>Language</label>
            <select title="language" id="language">
              <option value="fr" selected>Francais</option>
              <option value="en">English</option>
              <option value="es">Espanol</option>
              <option value="pt">Portugues</option>
              <option value="ar">Arabic (Latin)</option>
              <option value="wo">Wolof</option>
            </select>
          </div>
        </section>

        <!-- Items -->
        <section class="form-section">
          <h3 class="section-title">
            Items
            <button type="button" class="btn-add" id="add-item">+ Add Item</button>
          </h3>
          <div id="items-list"></div>
        </section>

        <!-- Taxes -->
        <section class="form-section">
          <h3 class="section-title">
            Taxes
            <button type="button" class="btn-add" id="add-tax">+ Add Tax</button>
          </h3>
          <p class="section-hint">Tax amounts are <strong>auto-calculated</strong> from subtotal.</p>
          <div id="taxes-list"></div>
        </section>

        <!-- Custom Text Lines -->
        <section class="form-section">
          <h3 class="section-title">
            Custom Lines
            <button type="button" class="btn-add" id="add-custom-line">+ Add</button>
          </h3>
          <p class="section-hint">Extra text lines inserted before totals.</p>
          <div id="custom-lines-list"></div>
        </section>

        <!-- Payment -->
        <section class="form-section">
          <h3 class="section-title">Payment</h3>
          <div class="field-row">
            <div class="field">
              <label>Received</label>
              <input title="received" type="number" id="received" value="100000" min="0" />
            </div>
            <div class="field">
              <label>Order Ref</label>
              <input title="order-ref" type="text" id="order-ref" value="ORD-2024-001" />
            </div>
          </div>
          <div class="field">
            <label>Served by</label>
            <input title="served-by" type="text" id="served-by" value="Mamadou" />
          </div>
        </section>

        <!-- Logo -->
        <section class="form-section">
          <h3 class="section-title">Logo</h3>
          <p class="section-hint">Upload an image — it will be dithered for thermal printing.</p>
          <div class="logo-upload-area" id="logo-upload-area">
            <input type="file" id="logo-file" accept="image/*" hidden />
            <canvas id="logo-canvas" hidden></canvas>
            <div class="logo-preview" id="logo-preview">
              <button type="button" class="btn-upload" id="btn-upload-logo">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
                Upload Logo
              </button>
              <button type="button" class="btn-remove-logo" id="btn-remove-logo" style="display:none" title="Remove logo">&times;</button>
              <img id="logo-img-preview" style="display:none;max-width:100%;max-height:80px;margin-top:6px;border-radius:4px" />
            </div>
          </div>
        </section>

        <!-- Options -->
        <section class="form-section">
          <h3 class="section-title">Options</h3>
          <div class="checkboxes">
            <label class="checkbox-label">
              <input type="checkbox" id="opt-date" checked /> Print date &amp; time
            </label>
            <label class="checkbox-label">
              <input type="checkbox" id="opt-receipt-no" checked /> Receipt number
            </label>
            <label class="checkbox-label">
              <input type="checkbox" id="opt-barcode" checked /> Barcode (Code128)
            </label>
            <label class="checkbox-label">
              <input type="checkbox" id="opt-qr" /> QR Code
            </label>
            <label class="checkbox-label">
              <input type="checkbox" id="opt-thankyou" checked /> Thank-you footer
            </label>
            <label class="checkbox-label">
              <input type="checkbox" id="opt-cut" checked /> Paper cut
            </label>
          </div>
          <div class="field" id="qr-data-field" style="display:none;margin-top:8px">
            <label>QR Data</label>
            <input title="qr-data" type="text" id="qr-data" value="https://thermoprint.dev" placeholder="URL or text" />
          </div>
          <div class="field" style="margin-top:8px">
            <label>Footer Alignment</label>
            <div class="align-btns" data-target="footer-align">
              <button type="button" class="align-btn active" data-align="center" title="Center">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="10" x2="6" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="18" y1="18" x2="6" y2="18"/></svg>
              </button>
              <button type="button" class="align-btn" data-align="left" title="Left">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>
              </button>
              <button type="button" class="align-btn" data-align="right" title="Right">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="21" y1="10" x2="7" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="7" y2="18"/></svg>
              </button>
            </div>
          </div>
        </section>

        <button type="submit" class="btn-build" id="build-btn">
          <span id="build-label">Build Receipt</span>
        </button>
      </form>
    </aside>

    <!-- ── Main: Preview + Output ── -->
    <main class="preview-pane">
      <div class="preview-toolbar">
        <span class="preview-title">Receipt Preview</span>
        <div class="toolbar-actions">
          <button class="btn-icon btn-print" id="print-btn" disabled title="Print via WebSerial/WebUSB">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>
            </svg>
            Print
          </button>
          <button class="btn-icon" id="export-png-btn" disabled title="Export as PNG">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
            </svg>
            PNG
          </button>
          <button class="btn-icon" id="export-pdf-btn" disabled title="Export as PDF">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
            </svg>
            PDF
          </button>
          <button class="btn-icon" id="copy-code-btn" disabled title="Copy JS code">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
            </svg>
            Copy JS
          </button>
          <button class="btn-icon" id="download-btn" disabled title="Download .bin">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
              <polyline points="7 10 12 15 17 10"/>
              <line x1="12" y1="15" x2="12" y2="3"/>
            </svg>
            .bin
          </button>
          <button class="btn-icon" id="copy-hex-btn" disabled title="Copy hex bytes">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
            </svg>
            Hex
          </button>
        </div>
      </div>

      <!-- Tabs: Preview | JS Code | JSON Template | Hex -->
      <div class="output-tabs">
        <button class="tab-btn active" data-tab="preview">Preview</button>
        <button class="tab-btn" data-tab="code">JS Code</button>
        <button class="tab-btn" data-tab="json">JSON Template</button>
        <button class="tab-btn" data-tab="hex">Hex Dump</button>
      </div>

      <!-- Tab: Preview -->
      <div class="tab-content active" id="tab-preview">
        <div class="receipt-wrapper">
          <div class="receipt-paper" id="receipt-paper">
            <div class="empty-state" id="empty-state">
              <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
                <rect x="8" y="14" width="32" height="24" rx="4" fill="#1e293b"/>
                <rect x="8" y="14" width="32" height="8" rx="4" fill="#e94560" opacity="0.7"/>
                <rect x="14" y="25" width="20" height="2" rx="1" fill="#334155"/>
                <rect x="16" y="29" width="16" height="12" rx="2" fill="#f1f5f9" opacity="0.9"/>
              </svg>
              <p>Fill in the form and click<br/><strong>Build Receipt</strong></p>
            </div>
            <div class="receipt-content" id="receipt-content" style="display:none"></div>
          </div>
        </div>
      </div>

      <!-- Tab: JS Code -->
      <div class="tab-content" id="tab-code">
        <div class="code-wrapper">
          <pre class="code-output" id="code-output">// Build your receipt to see the JS code here</pre>
        </div>
      </div>

      <!-- Tab: JSON Template -->
      <div class="tab-content" id="tab-json">
        <div class="code-wrapper">
          <div class="json-toolbar">
            <button class="btn-icon" id="copy-json-btn" disabled title="Copy JSON template">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
                <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
              </svg>
              Copy JSON
            </button>
            <span class="json-hint">Use <code>render_template(json)</code> to render this template</span>
          </div>
          <pre class="code-output json-output" id="json-output">// Build your receipt to see the JSON template here</pre>
        </div>
      </div>

      <!-- Tab: Hex Dump -->
      <div class="tab-content" id="tab-hex">
        <div class="hex-wrapper">
          <pre class="hex-dump" id="hex-dump"></pre>
        </div>
      </div>

      <!-- Stats bar -->
      <div class="stats-bar" id="stats-bar" style="display:none">
        <span id="stat-bytes">0 bytes</span>
        <span class="stat-sep">|</span>
        <span id="stat-items">0 items</span>
        <span class="stat-sep">|</span>
        <span id="stat-taxes">0 taxes</span>
        <span class="stat-sep">|</span>
        <span id="stat-total">Total: 0 FCFA</span>
      </div>
    </main>
  </div>

  <div class="toast" id="toast"></div>

  <script type="module" src="app.js"></script>
</body>
</html>