site/examples/quote-tool.html
live recomputestate machinedark<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VERSEVA Design · Example: quote tool</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@500..700&family=Manrope:wght@400..700&family=JetBrains+Mono:wght@400..600&display=swap">
<link rel="stylesheet" href="../../tokens.css?v=1p3">
<link rel="stylesheet" href="../../components.css?v=1p3">
<style>
* { margin: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text-1); font-family: var(--font-sans); font-size: 15px; line-height: 1.5; }
.crumb { position: sticky; top: 0; z-index: 10; background: var(--bg); display: flex; align-items: center; gap: var(--s-3); padding: var(--s-3) var(--s-5); border-bottom: 1px solid var(--hairline); font-size: 12.5px; color: var(--text-3); }
.crumb a { color: var(--text-2); text-decoration: none; font-family: var(--font-display); font-weight: var(--display-weight); }
.tool { max-width: 64rem; margin: 0 auto; padding: var(--s-6) var(--s-5) var(--s-8); display: grid; gap: var(--s-5); }
.toolhead { display: flex; align-items: center; gap: var(--s-4); }
.toolhead h1 { font-family: var(--font-display); font-weight: var(--heading-weight); font-size: 20px; letter-spacing: -0.015em; flex: 1; }
.split { display: grid; grid-template-columns: 1.1fr 1fr; gap: var(--s-5); align-items: start; }
.formgrid { display: grid; gap: var(--s-4); }
.formrow { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.sumfoot { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); padding-top: var(--s-4); }
.total b { font-family: var(--font-display); font-weight: var(--display-weight); font-size: 26px; letter-spacing: -0.01em; }
.total span { display: block; font-size: 11.5px; color: var(--text-3); }
@media (max-width: 820px) { .split, .formrow { grid-template-columns: 1fr; } }
.crumb a { display: inline-flex; align-items: center; gap: 8px; }
.crumb .vmark { width: 16px; height: 16px; flex: none; }
.crumb .codelink { margin-left: auto; }
</style>
</head>
<body>
<div class="crumb"><a href="../index.html"><svg class="vmark" viewBox="0 0 768 775.92" aria-hidden="true"><defs><clipPath id="vmk-a"><path d="M400 277h358v396.4H400z"/></clipPath><clipPath id="vmk-b"><path d="M10 97h550v576.4H10z"/></clipPath><clipPath id="vmk-c"><path d="M10 94.5h358V491H10z"/></clipPath><clipPath id="vmk-d"><path d="M208 94.5h550V671H208z"/></clipPath></defs><g clip-path="url(#vmk-a)"><path fill="currentColor" opacity=".55" d="M 558.605469 278.128906 C 555.410156 277.984375 552.164062 277.90625 548.96875 277.90625 C 466.261719 277.90625 407.433594 327.675781 401.222656 333.144531 L 400.800781 333.59375 C 400.800781 333.59375 587.386719 406.570312 718.046875 673.386719 C 718.046875 673.386719 767.757812 550.011719 756.207031 509.574219 C 740.042969 453 663.917969 362.328125 584.683594 279.359375 L 558.605469 278.128906"/></g><g clip-path="url(#vmk-b)"><path fill="currentColor" d="M 49.953125 673.386719 C 49.953125 673.386719 262.128906 253.59375 559.761719 253.59375 C 497.894531 190.457031 437.765625 134.679688 406.402344 106.136719 C 393.699219 94.574219 374.308594 94.574219 361.601562 106.136719 C 285.859375 175.0625 42.257812 402.917969 11.796875 509.574219 C 0.242188 550.011719 49.953125 673.386719 49.953125 673.386719"/></g><g clip-path="url(#vmk-c)"><path fill="currentColor" opacity=".55" d="M 209.394531 489.785156 C 212.589844 489.929688 215.835938 490.007812 219.03125 490.007812 C 301.738281 490.007812 360.566406 440.238281 366.777344 434.765625 L 367.199219 434.316406 C 367.199219 434.316406 180.613281 361.34375 49.953125 94.523438 C 49.953125 94.523438 0.242188 217.902344 11.792969 258.335938 C 27.957031 314.914062 104.082031 405.585938 183.316406 488.554688 L 209.394531 489.785156"/></g><g clip-path="url(#vmk-d)"><path fill="currentColor" d="M 718.046875 94.523438 C 718.046875 94.523438 505.871094 514.316406 208.238281 514.316406 C 270.105469 577.453125 330.234375 633.230469 361.597656 661.773438 C 374.300781 673.335938 393.691406 673.335938 406.398438 661.773438 C 482.140625 592.847656 725.742188 364.992188 756.203125 258.335938 C 767.757812 217.902344 718.046875 94.523438 718.046875 94.523438"/></g></svg><span>VERSEVA Design</span></a><span>examples</span><span>quote tool</span><a class="btn btn-secondary sm codelink" href="../code.html?f=examples/quote-tool.html">Code</a></div>
<div class="tool">
<div class="toolhead">
<h1>New quote</h1>
<div class="seg"><button type="button" aria-selected="true">Bottling</button><button type="button">Labeling</button><button type="button">Kitting</button></div>
</div>
<div class="split">
<div class="card">
<div class="hd"><h4>Request</h4></div>
<div class="bd formgrid">
<div class="field"><label for="q-client">Client</label><input id="q-client" value="Northline Foods"></div>
<div class="formrow">
<div class="field"><label for="q-units">Units</label><input id="q-units" class="mono" value="24,000"></div>
<div class="field"><label for="q-size">Bottle size</label><input id="q-size" value="500 ml PET"></div>
</div>
<div class="formrow">
<div class="field"><label for="q-cap">Closure</label><input id="q-cap" value="38 mm tamper-evident"></div>
<div class="field error"><label for="q-date">Needed by</label><input id="q-date" value="2026-09-08"><div class="help">Inside the 10-day floor: needs a rush ruling.</div></div>
</div>
<label class="sel switch-wrap"><input class="sw" type="checkbox" checked id="freight-toggle"><span class="switch"></span>Include freight estimate</label>
</div>
<div class="ft"><button class="btn btn-ghost" type="button" id="save-draft">Save draft</button><button class="btn btn-primary" type="button" id="price-it">Price it</button></div>
</div>
<div class="card">
<div class="hd"><h4>Quote summary</h4></div>
<div class="bd">
<div class="lrows">
<div class="lrow"><span class="tx"><b>Run setup</b><span class="metaline">machine 12 · changeover</span></span><span class="val mono" id="v-setup">1,840.00</span></div>
<div class="lrow"><span class="tx"><b id="fill-label">Fill + cap, 24,000 u</b><span class="metaline">rate card v18</span></span><span class="val mono" id="v-fill">9,360.00</span></div>
<div class="lrow"><span class="tx"><b>Materials</b><span class="metaline">sourced · 3 vendors</span></span><span class="val mono" id="v-mat">6,120.00</span></div>
<div class="lrow" id="row-freight"><span class="tx"><b>Freight estimate</b><span class="metaline">to Columbus OH</span></span><span class="val mono" id="v-freight">740.00</span></div>
</div>
<div class="sumfoot">
<span class="total"><b class="mono" id="v-total">18,060.00</b><span>every line carries provenance</span></span>
<span class="badge warn" id="rush-badge">Rush ruling needed</span>
</div>
</div>
<div class="ft"><button class="btn btn-secondary" type="button" id="provenance">Provenance</button><button class="btn btn-primary" type="button" id="issue">Issue quote</button></div>
</div>
</div>
</div>
<div id="toastwrap"></div>
<style>
#toastwrap .toast { position: fixed; right: var(--s-5); bottom: var(--s-5); z-index: 9; }
</style>
<script>
const $ = (id) => document.getElementById(id);
const SETUP = 1840, FILL_RATE = 0.39, MAT_RATE = 0.255, FREIGHT = 740, RUSH_FLOOR = '2026-09-15';
const fmt = (n) => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const units = () => {
const n = parseInt(($('q-units').value || '').replace(/[^\d]/g, ''), 10);
return Number.isFinite(n) && n > 0 ? n : 0;
};
function toast(text, cls) {
const wrap = $('toastwrap');
wrap.replaceChildren();
const t = document.createElement('div');
t.className = 'toast ' + (cls || 'ok');
t.innerHTML = '<span class="dot"></span>' + text;
wrap.append(t);
setTimeout(() => t.remove(), 2400);
}
function recalc() {
const u = units();
const freightOn = $('freight-toggle').checked;
const fill = u * FILL_RATE, mat = u * MAT_RATE, freight = freightOn ? FREIGHT : 0;
$('fill-label').textContent = 'Fill + cap, ' + u.toLocaleString('en-US') + ' u';
$('v-fill').textContent = fmt(fill);
$('v-mat').textContent = fmt(mat);
$('row-freight').hidden = !freightOn;
$('v-total').textContent = fmt(SETUP + fill + mat + freight);
const dateField = $('q-date').closest('.field');
const rush = $('q-date').value.trim() < RUSH_FLOOR;
dateField.classList.toggle('error', rush);
dateField.querySelector('.help').textContent = rush ? 'Inside the 10-day floor: needs a rush ruling.' : 'Inside the standard window.';
$('rush-badge').hidden = !rush;
}
['q-units', 'q-date'].forEach((id) => $(id).addEventListener('input', recalc));
$('freight-toggle').addEventListener('change', recalc);
$('price-it').addEventListener('click', () => { recalc(); toast('Priced: every line carries provenance.'); });
$('save-draft').addEventListener('click', () => toast('Draft saved to the desk.'));
$('provenance').addEventListener('click', () => toast('4 sources attached · rate card v18, 3 vendor quotes.'));
$('issue').addEventListener('click', () => {
if ($('q-date').value.trim() < RUSH_FLOOR) { toast('Blocked: rush ruling still open.', 'err'); return; }
toast('Quote issued to ' + ($('q-client').value || 'the client') + '.');
});
document.querySelectorAll('.seg button').forEach((b) => b.addEventListener('click', () => {
document.querySelectorAll('.seg button').forEach((x) => x.setAttribute('aria-selected', x === b ? 'true' : 'false'));
}));
recalc();
</script>
</body>
</html>