Files
design-v3/05-lists.html
2026-09-25 10:26:24 -04:00

77 lines
9.5 KiB
HTML

<style>
.namebar { padding: var(--space-4) var(--space-5); display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.part { padding: 6px 12px; border-radius: var(--radius-sm); border: 1px dashed var(--border-control); font-weight: 700; color: var(--text-secondary); }
.part.on { border: 2px solid var(--text-accent); background: var(--accent-wash); color: var(--text-accent); }
.part small { display: block; font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: .08em; }
.ltool { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; padding: var(--space-4); }
.ltool .input { width: auto; flex: 1; min-width: 180px; min-height: 38px; }
.rename { display: flex; gap: 6px; align-items: center; }
.rename .input { min-height: 34px; padding: 5px 10px; max-width: 280px; }
.used a { font-variant-numeric: tabular-nums; }
.addrow td { background: var(--surface-inset); }
@media (max-width: 699px) {
.tbl.stackable thead { display: none; }
.tbl.stackable tr { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; padding: 12px 14px; border-bottom: 1px solid var(--border-hairline); }
.tbl.stackable td { border: 0; padding: 0; }
.tbl.stackable td.acts { grid-column: 1 / -1; }
.tbl.stackable tr.addrow { grid-template-columns: 1fr; }
}
</style>
<script>
(function () {
var x = X;
var KINDS = [['brands', 'Brands', 0], ['speeds', 'Print speeds', 1], ['materials', 'Materials', 2], ['types', 'Filament types', 3], ['finishes', 'Finishes', 4], ['spool_types', 'Spool types', -1]];
var q = new URLSearchParams(location.search);
var S = { kind: q.get('list') || 'brands', f: '', unused: false, editing: null, conflict: false, addErr: false };
var SRC = { manufacturer: '<span class="prov"><i>◆</i>manufacturer</span>', community: '<span class="prov"><i>◇</i>community</span>', estimated: '<span class="prov warn"><i>≈</i>estimated</span>' };
function kindRow(k) { for (var i = 0; i < KINDS.length; i++) if (KINDS[i][0] === k) return KINDS[i]; }
function namebar() {
var k = kindRow(S.kind), parts = [['Sunlu', 'brand', 1], ['Rapid', 'speed', 0], ['PETG', 'material', 1], ['CF', 'type', 0], ['Silk', 'finish', 1]];
if (k[2] < 0) return '<section class="namebar nm-inset" aria-label="What this list does"><span class="small"><b>Spool types</b> are physical spool designs and what they weigh empty. A spool\'s empty spool weight comes from its type, never its brand, because one brand ships spools that differ by 100 g or more.</span></section>';
return '<section class="namebar nm-inset" aria-label="Where this list appears in a spool name"><span class="small muted" style="margin-right:6px">A spool name is built from</span>' + parts.map(function (p, i) { return '<span class="part' + (i === k[2] ? ' on' : '') + '">' + p[0] + '<small>' + p[1] + (p[2] ? '' : ' · opt') + '</small></span>'; }).join('') + '</section>';
}
function rows() {
var list = LOOKUPS[S.kind].filter(function (r) { return (!S.f || r[0].toLowerCase().indexOf(S.f.toLowerCase()) >= 0) && (!S.unused || !r[1]); });
var st = S.kind === 'spool_types', br = S.kind === 'brands';
var head = '<thead><tr><th>Name</th><th class="r">Used by</th>' + (st ? '<th class="r">Empty weight</th><th>Source</th>' : br ? '<th class="r hide-m">Brand empty spool weight <span class="sub" style="display:inline;text-transform:none">last resort</span></th>' : '') + '<th class="r"><span class="sr-only">Actions</span></th></tr></thead>';
var body = list.map(function (r, i) {
var name = r[0], used = r[1], editing = S.editing === name;
var nameCell = editing ? '<div class="rename"><input class="input" id="rn" value="' + x.esc(name) + '" aria-label="New name for ' + x.esc(name) + '"><button class="btn btn-sm btn-primary" type="button" data-rn-save>Save</button><button class="btn btn-sm" type="button" data-rn-cancel>Cancel</button></div>' +
(S.conflict ? '<p class="field-error" style="margin:6px 0 0">Someone renamed this to “' + x.esc(name) + ' 3D” after you loaded it. Nothing was saved. <button class="linkish" type="button" data-rn-reload>Reload and reapply</button></p>' : '') : '<b>' + x.esc(name) + '</b>';
return '<tr><td>' + nameCell + '</td><td class="r used">' + (used ? '<a href="02-filament.html?q=' + encodeURIComponent(name) + '">' + used + ' spool' + (used > 1 ? 's' : '') + '</a>' : '<span class="muted">unused</span>') + '</td>' +
(st ? '<td class="r num">' + x.g(r[2]) + '</td><td>' + SRC[r[3]] + '</td>' : br ? '<td class="r hide-m">' + (r[2] ? x.g(r[2]) : '<span class="muted">none</span>') + '</td>' : '') +
'<td class="r acts"><div class="row" style="gap:6px;justify-content:flex-end">' + (editing ? '' : '<button class="btn btn-sm" type="button" data-rn="' + x.esc(name) + '">Rename</button>') +
(used ? '<button class="btn btn-sm" type="button" disabled title="In use by ' + used + ' spools. Change those spools first; the database refuses to remove a value that is still used.">Remove</button>' : '<button class="btn btn-sm btn-danger" type="button" data-fake="Removed “' + x.esc(name) + '” from ' + x.esc(kindRow(S.kind)[1]) + '">Remove</button>') + '</div></td></tr>';
}).join('');
var add = '<tr class="addrow"><td colspan="' + (st ? 5 : br ? 4 : 3) + '"><div class="row" style="gap:8px"><input class="input" id="addv" style="max-width:280px;min-height:36px" placeholder="Add a ' + (st ? 'spool type, e.g. eSun — 1 kg cardboard' : 'value') + '" aria-label="New value">' +
(st ? '<input class="input num" style="max-width:110px;min-height:36px" placeholder="Empty g" aria-label="Empty spool weight in grams"><select class="select" style="width:auto;min-height:36px" aria-label="Where the empty spool weight came from"><option>Source…</option><option>manufacturer</option><option>community</option><option>estimated</option></select>' : '') +
'<button class="btn btn-sm btn-primary" type="button" data-add>Add</button></div>' +
(S.addErr ? '<p class="field-error" style="margin:8px 0 0">' + (st ? 'An empty spool weight and its source go together, or neither. 0 g is refused: leave it blank if nobody has weighed one.' : '“petg” looks like <b>PETG</b>, which already exists. Use PETG so spools group together. <span class="sub" style="display:inline">Proposed check: the database compares names case-sensitively today, so it would accept this.</span>') + '</p>' : '') + '</td></tr>';
return '<table class="tbl stackable">' + head + '<tbody>' + (body || '<tr><td colspan="5"><div class="empty"><h3>Nothing matches</h3><p>Try another filter.</p></div></td></tr>') + add + '</tbody></table>';
}
function render() {
var k = kindRow(S.kind);
return '<header class="page-head"><div><h1>Lists</h1><p class="sum">The values a spool\'s name is built from. <b>Operator</b> or admin can change them; values still in use can\'t be removed.</p></div></header>' +
'<div class="tabs" role="tablist" aria-label="Lists">' + KINDS.map(function (kk) { return '<button role="tab" type="button" data-kind="' + kk[0] + '" aria-selected="' + (S.kind === kk[0]) + '">' + kk[1] + ' <span class="n">' + LOOKUPS[kk[0]].length + '</span></button>'; }).join('') + '</div>' +
namebar() +
'<section class="nm-raised table-card"><div class="ltool"><input class="input" id="lf" type="search" value="' + x.esc(S.f) + '" placeholder="Filter ' + k[1].toLowerCase() + '…" aria-label="Filter"><button class="btn btn-sm" type="button" aria-pressed="' + S.unused + '" data-unused>Unused only</button>' +
'<span class="spacer"></span><span class="small muted">Demo</span><button class="btn btn-sm" type="button" data-demo-add>Show add error</button><button class="btn btn-sm" type="button" data-demo-conflict>Show rename conflict</button></div><div class="table-scroll">' + rows() + '</div></section>';
}
function redraw(fid) { var pos = fid && document.getElementById(fid) && document.getElementById(fid).selectionStart; document.getElementById('page').innerHTML = render(); if (fid && document.getElementById(fid)) { var el = document.getElementById(fid); el.focus(); if (pos != null) el.setSelectionRange(pos, pos); } history.replaceState(null, '', location.pathname + '?list=' + S.kind); }
document.addEventListener('click', function (e) {
var t = e.target, a;
if ((a = t.closest('[data-kind]'))) { S.kind = a.getAttribute('data-kind'); S.editing = null; S.conflict = S.addErr = false; return redraw(); }
if (t.closest('[data-unused]')) { S.unused = !S.unused; return redraw(); }
if ((a = t.closest('[data-rn]'))) { S.editing = a.getAttribute('data-rn'); S.conflict = false; redraw('rn'); return; }
if (t.closest('[data-rn-cancel]')) { S.editing = null; S.conflict = false; return redraw(); }
if (t.closest('[data-rn-save]')) { var v = document.getElementById('rn').value; S.editing = null; X.toast('Renamed to <b>' + x.esc(v) + '</b>. Every spool using it now reads the new name.'); return redraw(); }
if (t.closest('[data-rn-reload]')) { S.conflict = false; return redraw('rn'); }
if (t.closest('[data-demo-conflict]')) { S.editing = LOOKUPS[S.kind][0][0]; S.conflict = true; return redraw(); }
if (t.closest('[data-demo-add]')) { S.addErr = true; return redraw('addv'); }
if (t.closest('[data-add]')) { var nv = document.getElementById('addv').value.trim(); if (!nv) { S.addErr = true; return redraw('addv'); } S.addErr = false; X.toast('Added <b>' + x.esc(nv) + '</b> to ' + kindRow(S.kind)[1]); return redraw(); }
});
document.addEventListener('input', function (e) { if (e.target.id === 'lf') { S.f = e.target.value; redraw('lf'); } });
X.init({ name: 'lists' }, render);
})();
</script>