Files
Joshua KingandClaude Opus 5 d9ed1898b9 Add the Extrudex Design System v3 bundle
Tokens (light + dark, neumorphism, 54/54 WCAG AA contrast pairs), the
self-hosted JetBrains Mono fonts, and interactive mockups of every screen
across three iterations. Iteration 3 is final: phone, tablet and desktop
tiers, device preview, and "empty spool weight" terminology. The build
scripts regenerate the screens byte-identically from source.

Implementation is tracked in Maestro feature UI Refresh - V3.0
(224b72a1-8d84-4970-97ea-5dafa4d8ea3b).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 11:31:50 -04:00

287 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<style>
.scenarios { padding: var(--space-4) var(--space-5); display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
.form-wrap { position: relative; border-radius: var(--radius-xl); padding: var(--space-6); background: repeating-linear-gradient(135deg, transparent 0 14px, var(--accent-wash) 14px 15px); }
.sheet { max-width: 880px; margin: 0 auto; padding: 0; overflow: hidden; }
.sheet-head { display: flex; gap: var(--space-4); align-items: flex-start; padding: var(--space-6) var(--space-6) var(--space-4); border-bottom: 1px solid var(--border-hairline); }
.sheet-head h2 { margin: 0; font-size: var(--fs-h2); }
.sheet-body { padding: var(--space-5) var(--space-6); display: flex; flex-direction: column; gap: var(--space-6); }
.sheet-foot { padding: var(--space-4) var(--space-6); border-top: 1px solid var(--border-hairline); display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; justify-content: flex-end; }
fieldset.sec { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-4); }
fieldset.sec > legend { padding: 0; margin-bottom: var(--space-3); font-size: var(--fs-label); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); font-weight: var(--fw-bold); }
.fr { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field > .lab { display: flex; gap: var(--space-2); align-items: baseline; font-size: var(--fs-label); font-weight: var(--fw-bold); }
.field > .lab em { font-style: normal; font-weight: var(--fw-regular); color: var(--text-secondary); font-size: var(--fs-caption); }
.req { color: var(--text-accent); font-size: var(--fs-caption); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
.hint { font-size: var(--fs-caption); color: var(--text-secondary); line-height: var(--lh-snug); }
.has-error > .lab { color: var(--status-error); }
.has-error > .lab::before { content: "⚠"; }
.srv { font-size: var(--fs-caption); color: var(--text-secondary); }
.namebar { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.slots { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-2); }
.slot { padding: 8px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-control); background: var(--surface-raised); box-shadow: var(--nm-raised-1); display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.slot .k { font-size: var(--fs-caption); color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
.slot .v { font-weight: var(--fw-bold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.slot.empty { border-style: dashed; box-shadow: none; background: transparent; }
.slot.empty .v { color: var(--text-secondary); font-weight: var(--fw-regular); font-style: italic; }
.fullname { font-size: var(--fs-h3); font-weight: var(--fw-display); }
.weights { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: var(--space-5); align-items: start; }
.scale { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
.stackbar { display: flex; height: 26px; border-radius: var(--radius-pill); overflow: hidden; border: 1px solid var(--border-control); box-shadow: var(--nm-inset-1); background: var(--surface-inset); }
.stackbar .t { background: repeating-linear-gradient(45deg, var(--border-strong) 0 5px, transparent 5px 10px); border-right: 2px solid var(--text-primary); }
.stackbar .f { background: var(--text-accent); }
.stackbar .u { background: transparent; }
.eq { display: grid; grid-template-columns: auto 1fr auto; gap: 4px var(--space-3); font-size: 13px; font-variant-numeric: tabular-nums; }
.eq .tot { border-top: 1px solid var(--border-strong); padding-top: 4px; font-weight: var(--fw-bold); }
.key { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-control); vertical-align: -1px; }
.colors-row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.conflict, .errsum { padding: var(--space-5); border: 2px solid var(--status-warning); border-radius: var(--radius-lg); background: var(--wash-warning); display: flex; flex-direction: column; gap: var(--space-3); }
.errsum { border-color: var(--status-error); background: var(--wash-error); }
.conflict h3, .errsum h3 { margin: 0; font-size: var(--fs-body-lg); display: flex; gap: var(--space-2); align-items: center; }
.conflict h3 { color: var(--status-warning); } .errsum h3 { color: var(--status-error); }
.cmp th, .cmp td { padding: 8px 10px; }
.cmp .mine { color: var(--text-accent); font-weight: var(--fw-bold); }
.cmp .theirs { color: var(--status-warning); font-weight: var(--fw-bold); }
.specimen { padding: var(--space-6); }
.spec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--space-6) var(--space-5); align-items: start; }
.spec { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; }
.spec .cap { font-size: var(--fs-caption); color: var(--text-secondary); line-height: var(--lh-snug); }
.spec .cap b { color: var(--text-primary); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
.btn.is-hover { border-color: var(--text-accent); }
[data-only] { display: none; }
body.s-conflict [data-only~="conflict"], body.s-errors [data-only~="errors"], body.s-add [data-only~="add"], body.s-tare [data-only~="tare"], body.s-edit [data-only~="edit"] { display: revert; }
body.s-conflict .fr[data-only~="conflict"], body.s-errors .fr[data-only~="errors"] { display: grid; }
body.s-conflict .conflict[data-only], body.s-errors .errsum[data-only] { display: flex; }
@media (max-width: 760px) { .weights { grid-template-columns: 1fr; } .slots { grid-template-columns: repeat(2, minmax(0, 1fr)); } .form-wrap { padding: var(--space-2); } .sheet-head, .sheet-body, .sheet-foot { padding-left: var(--space-4); padding-right: var(--space-4); } }
</style>
<header class="page-head">
<div>
<h1>Add / Edit spool</h1>
<p class="muted">Opens as a dialog over Filament. Pick a scenario to see each state.</p>
</div>
<a class="btn" href="02-filament.html">← Back to Filament</a>
</header>
<section class="scenarios nm-raised" aria-label="Scenario">
<span class="lbl-t small muted">Scenario</span>
<div class="seg" role="group" aria-label="Form scenario" id="scen">
<button type="button" data-s="conflict" aria-pressed="true">409 · changed since opened</button>
<button type="button" data-s="errors" aria-pressed="false">400 · field errors</button>
<button type="button" data-s="tare" aria-pressed="false">Tare unknown</button>
<button type="button" data-s="add" aria-pressed="false">Add new</button>
</div>
</section>
<div class="form-wrap">
<section class="sheet nm-dialog" role="dialog" aria-labelledby="form-h" aria-modal="false">
<div class="sheet-head">
<span class="swatch" style="width:40px;height:40px"><i style="background:#1b1b1b"></i></span>
<div style="flex:1;min-width:0">
<h2 id="form-h"><span data-only="conflict errors tare edit">Edit spool #47</span><span data-only="add">Add spool</span></h2>
<p class="small muted" style="margin:0"><span data-only="conflict errors tare edit">Opened 09:32 at version 4 · created 2 Aug · label printed 3 Aug</span><span data-only="add">New spools carry no version: nobody could have read one yet.</span></p>
</div>
<button class="btn btn-icon" type="button" aria-label="Close">✕</button>
</div>
<div class="sheet-body">
<!-- 409 -->
<div class="conflict" role="alert" data-only="conflict">
<h3>⚠ This spool changed since you opened it. Nothing was saved.</h3>
<p style="margin:0">Someone weighed it at <b>09:39</b>, after you opened this form at 09:32 (version 4 → 5). Saving now would have put the old remaining weight back and lost that measurement.</p>
<div class="table-wrap" style="margin:0;padding:0">
<table class="tbl cmp">
<thead><tr><th>Field</th><th class="r">When you opened (v4)</th><th class="r">Now (v5)</th><th class="r">Your edit</th></tr></thead>
<tbody>
<tr><td>Remaining <abbr title="net filament">net</abbr></td><td class="r num">860 g</td><td class="r num theirs">812 g · weighed</td><td class="r muted">not changed</td></tr>
<tr><td>Empty spool (tare)</td><td class="r num">262 g</td><td class="r num">262 g</td><td class="r muted">not changed</td></tr>
<tr><td>Location</td><td class="r">Shelf A3</td><td class="r">Shelf A3</td><td class="r mine">A1 · AMS Lite · slot 1</td></tr>
<tr><td>Cost per kg</td><td class="r num">$22.79</td><td class="r num">$22.79</td><td class="r muted">not changed</td></tr>
</tbody>
</table>
</div>
<p class="small" style="margin:0">Your only change doesn't overlap theirs, so it's safe to reapply it to the new version.</p>
<div class="actions">
<button class="btn btn-primary" type="button">Reload and reapply my location change</button>
<button class="btn" type="button">Reload and discard my edit</button>
</div>
<p class="srv" style="margin:0">Server said (409): <code>this spool changed after you loaded it — reload it and reapply your edit, so a newer weigh-in is not overwritten (you loaded version 4, it is now version 5)</code></p>
</div>
<!-- 400 summary -->
<div class="errsum" role="alert" data-only="errors">
<h3>⚠ 2 fields need fixing. Nothing was saved.</h3>
<ul style="margin:0;padding-left:20px" class="small">
<li><a href="#f-remaining">Remaining is more than the spool holds</a></li>
<li><a href="#f-colorname">Color name is empty</a></li>
</ul>
</div>
<fieldset class="sec">
<legend>Name: 5 parts</legend>
<div class="namebar nm-inset">
<div class="slots" aria-label="Name parts">
<div class="slot"><span class="k">Brand <span class="req">req</span></span><span class="v" id="sl-b">Sunlu</span></div>
<div class="slot empty"><span class="k">Speed · opt</span><span class="v">none</span></div>
<div class="slot"><span class="k">Material <span class="req">req</span></span><span class="v" id="sl-m">PETG</span></div>
<div class="slot" id="sl-t"><span class="k">Type · opt</span><span class="v">CF</span></div>
<div class="slot"><span class="k">Finish <span class="req">req</span></span><span class="v">Matte</span></div>
</div>
<div class="row"><span class="small muted">Reads as</span><span class="fullname">Sunlu PETG CF Matte</span><span class="chip chip-neutral">Black</span></div>
</div>
<div class="fr">
<label class="field"><span class="lab">Brand <span class="req">required</span></span><select class="select"><option>Sunlu</option></select></label>
<label class="field"><span class="lab">Print speed <em>optional</em></span><select class="select"><option>None</option><option>Rapid</option><option>High Speed</option></select></label>
<label class="field"><span class="lab">Material <span class="req">required</span></span><select class="select"><option>PETG</option></select></label>
<label class="field"><span class="lab">Filament type <em>optional</em></span><select class="select"><option>CF</option></select></label>
<label class="field"><span class="lab">Finish <span class="req">required</span></span><select class="select"><option>Matte</option></select></label>
</div>
</fieldset>
<fieldset class="sec">
<legend>Color</legend>
<div class="fr">
<div class="field" id="f-colorname">
<label class="lab" for="cn">Color name <span class="req">required</span></label>
<input class="input" id="cn" value="Black" data-only="conflict tare add edit">
<div data-only="errors" class="field has-error" style="gap:6px">
<input class="input" value="" placeholder="e.g. Charcoal" aria-invalid="true" aria-describedby="cn-err">
<p class="field-error" id="cn-err" style="margin:0">Give the color a name, even "Unknown". <span class="srv">(server: <code>color_name is required</code>)</span></p>
</div>
</div>
<div class="field"><span class="lab">Colors <em>1–3, in order</em></span>
<div class="colors-row"><span class="swatch"><i style="background:#1b1b1b"></i></span><input class="input" value="#1b1b1b" style="max-width:120px" aria-label="Color 1 hex"><button class="btn btn-sm" type="button">+ Add color</button></div></div>
</div>
</fieldset>
<fieldset class="sec">
<legend>Weight: net filament vs what the scale reads</legend>
<div class="weights">
<div class="stack">
<div class="fr">
<label class="field"><span class="lab">Total <em>net, as sold</em></span><input class="input num" value="1000 g"><span class="hint">Filament only. A "1 kg spool" is 1000 g plus the spool.</span></label>
<div class="field" id="f-remaining">
<label class="lab" for="rem">Remaining <em>net</em></label>
<input class="input num" id="rem" value="812 g" data-only="conflict tare edit">
<input class="input num" value="600 g" data-only="add">
<div data-only="errors" class="field has-error" style="gap:6px">
<input class="input num is-error" value="1040 g" aria-invalid="true" aria-describedby="rem-err">
<p class="field-error" id="rem-err" style="margin:0">1,040 g is more than this spool holds (1,000 g net). Lower remaining, or correct total. <span class="srv">(server: <code>remaining_grams cannot exceed total_grams</code>)</span></p>
</div>
<span class="hint" data-only="conflict edit">⚖ From a scale reading on 18 Sep 09:39, not typed</span>
</div>
</div>
<label class="field"><span class="lab">Spool type <em>supplies the tare</em></span>
<select class="select"><option>Sunlu — 1 kg plastic · 140 g · community</option><option>Elegoo — 1 kg cardboard · 154 g · manufacturer</option><option>Generic — 1 kg spool (assumed) · 180 g · estimated</option></select></label>
<div class="field">
<span class="lab">Empty spool (tare) <em>this reel</em></span>
<div data-only="conflict errors edit">
<input class="input num" value="262 g" aria-describedby="tare-src">
<p class="hint" id="tare-src" style="margin:6px 0 0"><span class="chip chip-accent">this spool</span> Weighed bare on 14 Sep. It beats the spool type's 140 g (community).</p>
</div>
<div data-only="add">
<input class="input num" value="140 g" aria-describedby="tare-add">
<p class="hint" id="tare-add" style="margin:6px 0 0"><span class="chip chip-info">community</span> From the Sunlu — 1 kg plastic spool type. Measured by users and corroborated. Only saved on this spool if you weigh it.</p>
</div>
<div data-only="tare">
<input class="input" value="" placeholder="unknown: type it, or pick a spool type" aria-describedby="tare-unk">
<p class="hint" id="tare-unk" style="margin:6px 0 0"><span class="chip chip-warn chip-dashed">? unknown</span> No spool type chosen, and TECSONAR has no published tare. <b>Not treated as 0</b>: 0 would count the whole scale reading as filament.</p>
</div>
</div>
</div>
<div class="scale nm-raised">
<span class="lab-t small" style="font-weight:700">On the scale</span>
<div data-only="conflict errors edit">
<div class="stackbar" aria-hidden="true"><span class="t" style="width:20.8%"></span><span class="f" style="width:64.4%"></span><span class="u"></span></div>
<div class="eq" style="margin-top:12px">
<span><span class="key" style="background:repeating-linear-gradient(45deg,var(--border-strong) 0 3px,transparent 3px 6px)"></span></span><span>Empty spool (tare)</span><span>262 g</span>
<span><span class="key" style="background:var(--text-accent)"></span></span><span>Filament remaining (net)</span><span>812 g</span>
<span></span><span class="tot">Gross: should read</span><span class="tot">1,074 g</span>
</div>
<label class="field" style="margin-top:12px"><span class="lab">Weigh it <em>whole spool</em></span><input class="input num" placeholder="full reads 1,262 g"></label>
<span class="hint">Type what the scale shows and remaining is worked out for you: reading − 262 g.</span>
</div>
<div data-only="add">
<div class="stackbar" aria-hidden="true"><span class="t" style="width:12.3%"></span><span class="f" style="width:52.6%"></span><span class="u"></span></div>
<div class="eq" style="margin-top:12px">
<span><span class="key" style="background:repeating-linear-gradient(45deg,var(--border-strong) 0 3px,transparent 3px 6px)"></span></span><span>Empty spool (tare, community)</span><span>140 g</span>
<span><span class="key" style="background:var(--text-accent)"></span></span><span>Filament remaining (net)</span><span>600 g</span>
<span></span><span class="tot">Gross: should read</span><span class="tot">740 g</span>
</div>
<label class="field" style="margin-top:12px"><span class="lab">Weigh it <em>whole spool</em></span><input class="input num" placeholder="full reads 1,140 g"></label>
<span class="hint">Weighing saves the 140 g tare on this spool, marked community, alongside the reading.</span>
</div>
<div data-only="tare">
<div class="stackbar" aria-hidden="true" style="border-style:dashed"><span class="f" style="width:18.8%"></span></div>
<div class="eq" style="margin-top:12px">
<span></span><span>Empty spool (tare)</span><span class="muted">unknown</span>
<span><span class="key" style="background:var(--text-accent)"></span></span><span>Filament remaining (net)</span><span>188 g</span>
<span></span><span class="tot">Gross</span><span class="tot muted">unknown</span>
</div>
<label class="field" style="margin-top:12px"><span class="lab">Weigh it <em>whole spool</em></span><input class="input is-disabled" disabled placeholder="needs a tare first"></label>
<span class="hint">Set an empty-spool weight (pick a spool type, or weigh the bare spool) and you can weigh the whole spool instead of guessing.</span>
</div>
</div>
</div>
</fieldset>
<fieldset class="sec">
<legend>Cost and place</legend>
<div class="fr">
<label class="field"><span class="lab">Cost per kg</span><input class="input num" value="$22.79"></label>
<label class="field"><span class="lab">Location <em>optional</em></span>
<input class="input" value="Shelf A3" data-only="errors tare add">
<input class="input" value="A1 · AMS Lite · slot 1" data-only="conflict edit" style="border-color:var(--text-accent)">
</label>
</div>
</fieldset>
</div>
<div class="sheet-foot">
<span class="small muted" style="margin-right:auto" data-only="conflict">Save is off until you choose how to handle the newer version.</span>
<button class="btn" type="button">Cancel</button>
<button class="btn btn-primary is-disabled" type="button" disabled data-only="conflict">Save changes</button>
<button class="btn btn-primary" type="button" data-only="errors tare edit">Save changes</button>
<button class="btn btn-primary" type="button" data-only="add">Add spool</button>
</div>
</section>
</div>
<section class="specimen nm-raised" aria-labelledby="spec-h">
<h2 class="section" id="spec-h">Control states: v3 specimen</h2>
<p class="small muted" style="margin-top:0">Every state changes something other than shadow direction. Switch themes with the toggle to check both.</p>
<div class="spec-grid">
<div class="spec"><button class="btn" type="button">Save</button><span class="cap"><b>Rest</b><br>raised + 1px border (3:1)</span></div>
<div class="spec"><button class="btn is-hover" type="button">Save</button><span class="cap"><b>Hover</b><br>border turns accent</span></div>
<div class="spec"><button class="btn is-pressed" type="button">Low stock</button><span class="cap"><b>Pressed / on</b><br>inset + wash fill + ✓ + accent border</span></div>
<div class="spec"><button class="btn is-focused" type="button">Save</button><span class="cap"><b>Focused</b><br>2px outline, 3px offset</span></div>
<div class="spec"><button class="btn" disabled type="button">Save</button><span class="cap"><b>Disabled</b><br>flat + dashed border + muted text</span></div>
<div class="spec"><button class="btn btn-primary" type="button">Add spool</button><span class="cap"><b>Primary</b><br>ink on light violet (6.14:1 dark)</span></div>
<div class="spec"><button class="btn btn-primary is-pressed" type="button">Add spool</button><span class="cap"><b>Primary pressed</b><br>deeper fill + ✓ + ring</span></div>
<div class="spec"><button class="btn btn-danger" type="button">Delete</button><span class="cap"><b>Danger</b><br>red text + red border</span></div>
<div class="spec" style="width:100%"><input class="input" placeholder="Rest"><span class="cap"><b>Input rest</b><br>inset + border</span></div>
<div class="spec" style="width:100%"><input class="input is-focused" value="Focused"><span class="cap"><b>Input focus</b><br>outline, border unchanged</span></div>
<div class="spec" style="width:100%"><input class="input" disabled value="Disabled"><span class="cap"><b>Input disabled</b><br>dashed, flat</span></div>
<div class="spec field has-error" style="width:100%;gap:6px"><input class="input" value="1040 g" aria-invalid="true"><p class="field-error" style="margin:0">Too much</p><span class="cap"><b>Input error</b><br>2px red + ⚠ + message</span></div>
</div>
</section>
<script>
(function(){
function set(s){document.body.className='s-'+s;
document.querySelectorAll('#scen [data-s]').forEach(function(b){b.setAttribute('aria-pressed',String(b.getAttribute('data-s')===s))});
var tareSheet = s==='tare';
document.querySelector('.sheet-head .swatch').innerHTML = tareSheet ? '<i style="background:#161616"></i><i style="background:#c0232f"></i>' : '<i style="background:#1b1b1b"></i>';
document.querySelector('.fullname').textContent = tareSheet ? 'TECSONAR PLA Matte' : 'Sunlu PETG CF Matte';
document.getElementById('sl-b').textContent = tareSheet ? 'TECSONAR' : 'Sunlu';
document.getElementById('sl-m').textContent = tareSheet ? 'PLA' : 'PETG';
var t = document.getElementById('sl-t'); t.className = tareSheet ? 'slot empty' : 'slot'; t.querySelector('.v').textContent = tareSheet ? 'none' : 'CF';
document.querySelector('.sheet-head p span').textContent = tareSheet ? 'Opened 09:32 at version 2 · created 2 Aug' : 'Opened 09:32 at version 4 · created 2 Aug · label printed 3 Aug';
document.getElementById('form-h').firstElementChild.textContent = tareSheet ? 'Edit spool #44' : 'Edit spool #47';
}
document.addEventListener('click',function(e){var b=e.target.closest('#scen [data-s]'); if(b) set(b.getAttribute('data-s'));});
document.addEventListener('DOMContentLoaded',function(){ set(location.hash==='#add'?'add':'conflict'); });
})();
</script>