|
| 1 | +<!doctype html> |
| 2 | +<!-- |
| 3 | + LAVISH-SAFE DECISION BOARD TEMPLATE |
| 4 | + Copy this file, replace the placeholder content, and verify that the browser |
| 5 | + audit reports zero error-severity layout warnings before announcing it ready. |
| 6 | +
|
| 7 | + Preserve these layout constraints: |
| 8 | + 1. Text line-height is never 1. Display text is at least 1.2, body text is at |
| 9 | + least 1.5, and headings are at least 1.35. |
| 10 | + 2. Large display text keeps 2px bottom padding and letter-spacing no tighter |
| 11 | + than -0.01em so the glyph box is not clipped. |
| 12 | + 3. Headings remain sans-serif with neutral tracking, right padding, and |
| 13 | + bottom spacing. |
| 14 | + 4. Every flex or grid child that holds text keeps min-width:0 and |
| 15 | + overflow-wrap:anywhere. |
| 16 | + 5. Cards and rows keep enough vertical padding that line boxes do not touch |
| 17 | + a border. |
| 18 | +--> |
| 19 | +<html lang="en"> |
| 20 | +<head> |
| 21 | +<meta charset="utf-8"> |
| 22 | +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 23 | +<title>Decision board</title> |
| 24 | +<style> |
| 25 | + :root{ |
| 26 | + --bg:#f4f6f8; --panel:#fff; --panel-2:#fbfcfd; --ink:#141a1f; --ink-2:#4a5560; |
| 27 | + --line:#e2e7ec; --line-2:#d3dae1; --accent:#1f6feb; --accent-soft:#e7f0ff; |
| 28 | + --amber:#b7791f; --amber-soft:#fdf3e2; --green:#1a7f5a; --green-soft:#e3f5ec; |
| 29 | + --grey:#6b7680; --grey-soft:#eef1f4; --red:#c0392b; --red-soft:#fbe9e6; |
| 30 | + --shadow:0 1px 2px rgba(20,26,31,.06),0 6px 20px rgba(20,26,31,.05); |
| 31 | + --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; |
| 32 | + --mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace; |
| 33 | + } |
| 34 | + @media (prefers-color-scheme:dark){:root{ |
| 35 | + --bg:#0e1216; --panel:#161c22; --panel-2:#131920; --ink:#e6ebef; --ink-2:#9aa6b1; |
| 36 | + --line:#232c35; --line-2:#2c3742; --accent:#589bff; --accent-soft:#16283f; |
| 37 | + --amber:#e0b25a; --amber-soft:#2a2313; --green:#5fcf9e; --green-soft:#12271e; |
| 38 | + --grey:#8b96a1; --grey-soft:#1c242c; --red:#f0857a; --red-soft:#2c1714; |
| 39 | + --shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.28); |
| 40 | + }} |
| 41 | + :root[data-theme="light"]{color-scheme:light; |
| 42 | + --bg:#f4f6f8;--panel:#fff;--panel-2:#fbfcfd;--ink:#141a1f;--ink-2:#4a5560;--line:#e2e7ec;--line-2:#d3dae1; |
| 43 | + --accent:#1f6feb;--accent-soft:#e7f0ff;--amber:#b7791f;--amber-soft:#fdf3e2;--green:#1a7f5a;--green-soft:#e3f5ec; |
| 44 | + --grey:#6b7680;--grey-soft:#eef1f4;--red:#c0392b;--red-soft:#fbe9e6; |
| 45 | + --shadow:0 1px 2px rgba(20,26,31,.06),0 6px 20px rgba(20,26,31,.05);} |
| 46 | + :root[data-theme="dark"]{color-scheme:dark; |
| 47 | + --bg:#0e1216;--panel:#161c22;--panel-2:#131920;--ink:#e6ebef;--ink-2:#9aa6b1;--line:#232c35;--line-2:#2c3742; |
| 48 | + --accent:#589bff;--accent-soft:#16283f;--amber:#e0b25a;--amber-soft:#2a2313;--green:#5fcf9e;--green-soft:#12271e; |
| 49 | + --grey:#8b96a1;--grey-soft:#1c242c;--red:#f0857a;--red-soft:#2c1714; |
| 50 | + --shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.28);} |
| 51 | + |
| 52 | + *{box-sizing:border-box;min-width:0} |
| 53 | + body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans); |
| 54 | + font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow-wrap:anywhere} |
| 55 | + h1,h2,h3{font-family:var(--sans);letter-spacing:0} |
| 56 | + h1{font-size:22px;line-height:1.3;margin:0} |
| 57 | + h3{font-size:16.5px;line-height:1.38;font-weight:650;margin:0 0 9px;padding-right:2px} |
| 58 | + p{margin:0 0 12px;overflow-wrap:anywhere} |
| 59 | + .wrap{max-width:1060px;margin:0 auto;padding:32px 22px 80px} |
| 60 | + |
| 61 | + .summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:0 0 30px} |
| 62 | + @media (max-width:720px){.summary{grid-template-columns:repeat(2,minmax(0,1fr))}} |
| 63 | + .stat{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 15px;box-shadow:var(--shadow)} |
| 64 | + .stat .n{font-size:26px;font-weight:700;line-height:1.2;letter-spacing:-.01em;padding-bottom:2px;font-variant-numeric:tabular-nums} |
| 65 | + .stat .l{font-size:12px;color:var(--ink-2);margin-top:6px} |
| 66 | + |
| 67 | + .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px;margin:0 0 16px;box-shadow:var(--shadow)} |
| 68 | + .card.decision{border-left:3px solid var(--amber)} |
| 69 | + .pill{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:650;padding:2px 9px;border-radius:999px;white-space:nowrap} |
| 70 | + .pill.amber{background:var(--amber-soft);color:var(--amber)} .pill.green{background:var(--green-soft);color:var(--green)} |
| 71 | + .rec{background:var(--green-soft);border-radius:9px;padding:9px 12px;font-size:13.5px;margin:0 0 14px} |
| 72 | + form.q{border-top:1px dashed var(--line-2);padding-top:13px;margin:0} |
| 73 | + form.q .opts{display:flex;flex-direction:column;gap:8px;margin:0 0 12px} |
| 74 | + form.q label{display:flex;gap:10px;align-items:flex-start;padding:10px 12px;border:1px solid var(--line);border-radius:9px;cursor:pointer;font-size:14px} |
| 75 | + form.q label:has(input:checked){border-color:var(--accent);background:var(--accent-soft)} |
| 76 | + form.q input[type=radio]{margin-top:2px;accent-color:var(--accent);width:16px;height:16px;flex:none} |
| 77 | + .selection-status{font-size:13px;color:var(--ink-2);margin:0 0 10px;min-height:20px} |
| 78 | + .submission-status{font-size:13px;color:var(--red);margin:10px 0 0;min-height:20px} |
| 79 | + button{font-family:inherit;font-size:13.5px;font-weight:650;border-radius:9px;cursor:pointer;border:1px solid var(--accent);background:var(--accent);color:#fff;padding:8px 15px} |
| 80 | +</style> |
| 81 | +</head> |
| 82 | +<body> |
| 83 | +<div class="wrap"> |
| 84 | + <h1>Decision board title</h1> |
| 85 | + |
| 86 | + <!-- Choices stay local until the single Send answers click queues and sends every answer synchronously. --> |
| 87 | + <div class="card decision"> |
| 88 | + <span class="pill amber">decision needed</span> |
| 89 | + <h3>What should be decided?</h3> |
| 90 | + <p>State the relevant context in one or two sentences.</p> |
| 91 | + <div class="rec"><b>Recommendation:</b> Choose option A because it best matches the stated goal.</div> |
| 92 | + <form class="q" data-lavish-question="decision-key" data-lavish-label="Short decision label"> |
| 93 | + <div class="opts"> |
| 94 | + <label><input type="radio" name="decision-key" value="A - first option (recommended)" required> A. First option</label> |
| 95 | + <label><input type="radio" name="decision-key" value="B - second option"> B. Second option</label> |
| 96 | + </div> |
| 97 | + <p class="selection-status" data-selection-status role="status" aria-live="polite">No answer selected.</p> |
| 98 | + </form> |
| 99 | + </div> |
| 100 | + |
| 101 | + <button type="button" data-send-answers onclick="sendAnswers()">Send answers</button> |
| 102 | + <p class="submission-status" data-submission-status role="alert" aria-live="assertive"></p> |
| 103 | +</div> |
| 104 | +<script> |
| 105 | + document.querySelectorAll('form.q').forEach(function(form){ |
| 106 | + form.addEventListener('change',function(){ |
| 107 | + var key=(form.dataset.lavishQuestion||'').trim(); |
| 108 | + if(!key){ |
| 109 | + form.querySelector('[data-selection-status]').textContent= |
| 110 | + 'Configuration error: add a unique data-lavish-question value to this decision form.'; |
| 111 | + return; |
| 112 | + } |
| 113 | + var choice=new FormData(form).get(key); |
| 114 | + form.querySelector('[data-selection-status]').textContent= |
| 115 | + choice?'Selected locally (not sent): '+choice:'No answer selected.'; |
| 116 | + }); |
| 117 | + }); |
| 118 | + |
| 119 | + var submissionInFlight=false; |
| 120 | + |
| 121 | + function sendAnswers(){ |
| 122 | + var forms=Array.from(document.querySelectorAll('form.q')); |
| 123 | + var answers=[]; |
| 124 | + var firstIncomplete=null; |
| 125 | + var sendButton=document.querySelector('[data-send-answers]'); |
| 126 | + var submissionStatus=document.querySelector('[data-submission-status]'); |
| 127 | + if(submissionInFlight)return; |
| 128 | + function failConfiguration(message){ |
| 129 | + sendButton.disabled=true; |
| 130 | + sendButton.textContent='Cannot send answers'; |
| 131 | + submissionStatus.textContent=message; |
| 132 | + } |
| 133 | + if(!forms.length){ |
| 134 | + failConfiguration('Configuration error: add at least one decision form before serving this board.'); |
| 135 | + return; |
| 136 | + } |
| 137 | + var keyedForms=forms.map(function(form,index){ |
| 138 | + return {form:form,key:(form.dataset.lavishQuestion||'').trim(),index:index}; |
| 139 | + }); |
| 140 | + var missingKey=keyedForms.find(function(item){return !item.key;}); |
| 141 | + if(missingKey){ |
| 142 | + failConfiguration('Configuration error: decision form '+(missingKey.index+1)+' needs a unique nonempty data-lavish-question value.'); |
| 143 | + return; |
| 144 | + } |
| 145 | + var expectedQuestionKeys=keyedForms.map(function(item){return item.key;}); |
| 146 | + var uniqueQuestionKeys=new Set(expectedQuestionKeys); |
| 147 | + if(uniqueQuestionKeys.size!==expectedQuestionKeys.length){ |
| 148 | + failConfiguration('Configuration error: every decision form needs a unique data-lavish-question value.'); |
| 149 | + return; |
| 150 | + } |
| 151 | + keyedForms.forEach(function(item){ |
| 152 | + item.controls=Array.from(item.form.elements).filter(function(control){ |
| 153 | + return control.matches('input[type="radio"],select')&& |
| 154 | + !control.matches(':disabled')&&control.name===item.key; |
| 155 | + }); |
| 156 | + }); |
| 157 | + var missingControl=keyedForms.find(function(item){return !item.controls.length;}); |
| 158 | + if(missingControl){ |
| 159 | + failConfiguration('Configuration error: decision form '+(missingControl.index+1)+ |
| 160 | + ' needs an enabled radio or select named "'+missingControl.key+'".'); |
| 161 | + return; |
| 162 | + } |
| 163 | + keyedForms.forEach(function(item){ |
| 164 | + var form=item.form; |
| 165 | + var key=item.key; |
| 166 | + var choice=new FormData(form).get(key); |
| 167 | + if(typeof choice!=='string'||!choice.trim()){ |
| 168 | + firstIncomplete=firstIncomplete||item; |
| 169 | + return; |
| 170 | + } |
| 171 | + answers.push({key:key,label:form.dataset.lavishLabel||key,answer:choice}); |
| 172 | + }); |
| 173 | + if(firstIncomplete){ |
| 174 | + var incompleteLabel=firstIncomplete.form.dataset.lavishLabel||firstIncomplete.key; |
| 175 | + submissionStatus.textContent='Answer required: choose an option for "'+incompleteLabel+'".'; |
| 176 | + firstIncomplete.form.querySelector('[data-selection-status]').textContent= |
| 177 | + 'No answer selected. Choose an option before sending.'; |
| 178 | + firstIncomplete.form.reportValidity(); |
| 179 | + firstIncomplete.controls[0].focus(); |
| 180 | + return; |
| 181 | + } |
| 182 | + if(!window.lavish||!window.lavish.queuePrompt||!window.lavish.sendQueuedPrompts)return; |
| 183 | + var batch={ |
| 184 | + submission:'explicit-send-batch', |
| 185 | + manifest:{expectedQuestionKeys:expectedQuestionKeys,expectedCount:expectedQuestionKeys.length}, |
| 186 | + answers:answers |
| 187 | + }; |
| 188 | + submissionInFlight=true; |
| 189 | + sendButton.disabled=true; |
| 190 | + submissionStatus.textContent=''; |
| 191 | + window.lavish.queuePrompt('Explicitly submitted decision batch: '+JSON.stringify(batch), |
| 192 | + {tag:'decision-batch',text:'Explicitly submitted decision batch',element:sendButton, |
| 193 | + queueKey:'explicit-decision-batch',data:batch}); |
| 194 | + window.lavish.sendQueuedPrompts(); |
| 195 | + forms.forEach(function(form,index){ |
| 196 | + form.querySelector('[data-selection-status]').textContent='Sent explicitly: '+answers[index].answer; |
| 197 | + }); |
| 198 | + } |
| 199 | +</script> |
| 200 | +</body> |
| 201 | +</html> |
0 commit comments