← back to Cncp Failures Mockups
v7 UI hooks: 8 band quick-jump pills, per-band modal/toast handlers, mark-resolved flow, Esc + × close, toddler celebration, elder confirm-then-resolve
788f2be00dae55a942cf73822539a1e0bb26aad7 · 2026-05-11 16:04:39 -0700 · SteveStudio2
Files touched
M public/v7-age-adaptive.html
Diff
commit 788f2be00dae55a942cf73822539a1e0bb26aad7
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Mon May 11 16:04:39 2026 -0700
v7 UI hooks: 8 band quick-jump pills, per-band modal/toast handlers, mark-resolved flow, Esc + × close, toddler celebration, elder confirm-then-resolve
---
public/v7-age-adaptive.html | 194 ++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 187 insertions(+), 7 deletions(-)
diff --git a/public/v7-age-adaptive.html b/public/v7-age-adaptive.html
index d2de4be..7ab906d 100644
--- a/public/v7-age-adaptive.html
+++ b/public/v7-age-adaptive.html
@@ -138,9 +138,41 @@
.meta { color:var(--muted); font-size:var(--font-size-small); display:flex; gap:8px; flex-wrap:wrap; }
.footer { display:flex; gap:8px; justify-content:space-between; align-items:center; }
- button.action-btn { background:var(--accent); color:#fff; border:none; padding:6px 12px; border-radius:var(--radius); font-size:var(--font-size-small); cursor:pointer; font-family:var(--font); font-weight:600; }
+ button.action-btn { background:var(--accent); color:#fff; border:none; padding:6px 12px; border-radius:var(--radius); font-size:var(--font-size-small); cursor:pointer; font-family:var(--font); font-weight:600; transition: transform 0.1s; }
+ button.action-btn:hover { transform: translateY(-1px); filter: brightness(1.1); }
+ button.action-btn:active { transform: translateY(0); filter: brightness(0.95); }
body[data-band="senior"] button.action-btn, body[data-band="elder"] button.action-btn { padding:18px 28px; font-size:1.2em; }
body[data-band="toddler"] button.action-btn { padding:22px 32px; font-size:1.4em; border-radius:32px; }
+
+ /* Band quick-jump pills */
+ .pills { display:flex; gap:6px; flex-wrap:wrap; margin-top:6px; flex-basis:100%; }
+ .pill-btn { background:transparent; border:1px solid var(--border); color:var(--muted); padding:4px 10px; border-radius:14px; cursor:pointer; font-size:11px; text-transform:uppercase; letter-spacing:1px; font-family:var(--font); transition: all 0.15s; }
+ .pill-btn:hover { color:var(--fg); border-color:var(--accent); }
+ .pill-btn.active { background:var(--accent); color:#fff; border-color:var(--accent); }
+
+ /* Detail modal — opens on any band's primary action */
+ .modal-back { position:fixed; inset:0; background:rgba(0,0,0,0.6); display:none; align-items:center; justify-content:center; z-index:100; padding:24px; }
+ .modal-back.open { display:flex; }
+ .modal { background:var(--panel); color:var(--fg); border:2px solid var(--border); border-radius:var(--radius); max-width:560px; width:100%; padding:var(--padding); position:relative; max-height:85vh; overflow:auto; }
+ body[data-band="toddler"] .modal, body[data-band="elder"] .modal { max-width:480px; }
+ body[data-band="elder"] .modal { border-width:3px; }
+ .modal h3 { margin:0 0 12px; }
+ .modal .close-x { position:absolute; top:8px; right:14px; background:none; border:none; color:var(--muted); font-size:24px; cursor:pointer; }
+ .modal-row { display:grid; grid-template-columns:90px 1fr; gap:10px; padding:6px 0; border-bottom:1px solid var(--border); font-size:var(--font-size-body); }
+ .modal-row:last-child { border-bottom:none; }
+ .modal-row .k { color:var(--muted); text-transform:uppercase; font-size:0.75em; letter-spacing:1px; }
+ .modal-actions { display:flex; gap:10px; margin-top:14px; flex-wrap:wrap; }
+ .toddler-celebrate { font-size:120px; text-align:center; line-height:1; animation: pop 0.5s ease; }
+ @keyframes pop { 0% { transform:scale(0); } 70% { transform:scale(1.2); } 100% { transform:scale(1); } }
+ /* hide certain modal pieces per band */
+ body[data-band="toddler"] .modal-row, body[data-band="kid"] .modal-row.adult-only { display:none; }
+ body[data-band="elder"] .modal-row.adult-only { display:none; }
+
+ /* Toast for transient feedback (mark resolved, dismissed, etc.) */
+ .toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:var(--accent); color:#fff; padding:14px 24px; border-radius:var(--radius); font-weight:600; box-shadow: 0 8px 24px rgba(0,0,0,0.3); z-index:200; opacity:0; transition: opacity 0.25s, bottom 0.25s; pointer-events:none; }
+ .toast.show { opacity:1; bottom:40px; }
+ body[data-band="toddler"] .toast { font-size:1.6em; padding:24px 36px; }
+ body[data-band="elder"] .toast { font-size:1.4em; padding:20px 30px; }
</style>
</head><body data-band="toddler">
@@ -151,8 +183,20 @@
</div>
<input id="ageSlider" type="range" min="3" max="95" value="3" />
<div class="tagline" id="tagline">Primary colors, huge taps. One face = good. One face = sad.</div>
+ <div class="pills" id="bandPills"></div>
+</div>
+
+<!-- Detail modal — populated dynamically when a card action fires -->
+<div class="modal-back" id="modalBack" onclick="if(event.target===this) closeModal()">
+ <div class="modal" id="modal">
+ <button class="close-x" onclick="closeModal()" aria-label="Close">×</button>
+ <div id="modalBody"></div>
+ </div>
</div>
+<!-- Toast for transient feedback -->
+<div class="toast" id="toast"></div>
+
<div class="container">
<h1 id="hed">Failures</h1>
<div class="sub" id="dek">A reading of what's going wrong, tuned to your age.</div>
@@ -191,11 +235,33 @@ function applyBand(age) {
bandLabel.textContent = b.label;
tagline.textContent = b.tagline;
hed.textContent = b.hed;
+ renderPills(b.id);
renderGrid(b);
}
slider.addEventListener('input', e => applyBand(Number(e.target.value)));
+// Quick-jump pills — one per band, snap slider to a representative age in that band.
+const BAND_PRESETS = [
+ { id:'toddler', age:3, label:'Toddler' },
+ { id:'kid', age:7, label:'Kid' },
+ { id:'tween', age:11, label:'Tween' },
+ { id:'teen', age:16, label:'Teen' },
+ { id:'adult', age:30, label:'Adult' },
+ { id:'mature', age:52, label:'Mature' },
+ { id:'senior', age:70, label:'Senior' },
+ { id:'elder', age:88, label:'Elder' }
+];
+function renderPills(activeId) {
+ document.getElementById('bandPills').innerHTML = BAND_PRESETS.map(b =>
+ `<button class="pill-btn${b.id===activeId?' active':''}" data-band="${b.id}" onclick="jumpToBand(${b.age})">${b.label}</button>`
+ ).join('');
+}
+function jumpToBand(age) {
+ slider.value = age;
+ applyBand(age);
+}
+
let _cachedData = null;
async function loadData() {
const r = await fetch('/data' + location.search);
@@ -236,12 +302,13 @@ function renderGrid(b) {
else if (b.id === 'elder') items = items.slice(0, 1);
else if (b.id === 'senior') items = items.slice(0, 3);
- grid.innerHTML = items.map(f => {
+ grid.innerHTML = items.map((f, idx) => {
const sevClass = f.severity || 'medium';
const ageBand = b.id;
if (ageBand === 'toddler') {
+ // entire card is the action — tap the face to celebrate
return `
- <div class="card">
+ <div class="card" onclick="onAction('${b.id}','${f.id}', this)" style="cursor:pointer;">
<div class="face">${faceFor(f.severity, f.status)}</div>
<div class="toddler-label">${plainLabel(f)}</div>
</div>`;
@@ -256,7 +323,7 @@ function renderGrid(b) {
<div class="last">${(f.lastLines||'').split('\\n')[0].slice(0,80)}</div>
</div>
</div>
- <button class="action-btn">Tell me more</button>
+ <button class="action-btn" onclick="onAction('${b.id}','${f.id}', this)">Tell me more</button>
</div>`;
}
if (ageBand === 'elder') {
@@ -264,7 +331,7 @@ function renderGrid(b) {
<div class="card">
<div class="action">${f.suggestedAction || 'Click here when ready.'}</div>
<div class="name">${f.processName}</div>
- <button class="action-btn">Done</button>
+ <button class="action-btn" onclick="onAction('${b.id}','${f.id}', this)">Done</button>
</div>`;
}
if (ageBand === 'senior') {
@@ -276,7 +343,7 @@ function renderGrid(b) {
</div>
<div class="last">${(f.lastLines||'').split('\\n')[0].slice(0,120)}</div>
${f.suggestedAction ? `<div class="action">${f.suggestedAction}</div>` : ''}
- <button class="action-btn">View details</button>
+ <button class="action-btn" onclick="onAction('${b.id}','${f.id}', this)">View details</button>
</div>`;
}
// adult / mature / tween / teen — full standard card with band-specific styling via CSS vars
@@ -292,7 +359,7 @@ function renderGrid(b) {
${f.suggestedAction ? `<div class="action">→ ${f.suggestedAction}</div>` : ''}
<div class="footer">
<span style="font-size:var(--font-size-small); color:var(--muted);">${new Date(f.lastSeen||f.created).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</span>
- <button class="action-btn">Open</button>
+ <button class="action-btn" onclick="onAction('${b.id}','${f.id}', this)">Open</button>
</div>
</div>`;
}).join('');
@@ -316,6 +383,119 @@ function renderGrid(b) {
}
}
+// ===== UI HOOKS — every band's primary action wired here. =====
+// In-memory state for the demo (no API writes); persists for the page session.
+const _localActions = new Map(); // failureId -> { resolved, dismissed, refined, opened, celebrated }
+
+function toast(msg, ms = 1800) {
+ const el = document.getElementById('toast');
+ el.textContent = msg;
+ el.classList.add('show');
+ setTimeout(() => el.classList.remove('show'), ms);
+}
+
+function openModal(html) {
+ document.getElementById('modalBody').innerHTML = html;
+ document.getElementById('modalBack').classList.add('open');
+ // ESC to close
+ document.addEventListener('keydown', _escHandler);
+}
+function closeModal() {
+ document.getElementById('modalBack').classList.remove('open');
+ document.removeEventListener('keydown', _escHandler);
+}
+function _escHandler(e) { if (e.key === 'Escape') closeModal(); }
+
+function findFailure(id) {
+ return (_cachedData && _cachedData.failures || []).find(f => f.id === id);
+}
+
+function detailHTML(f, band) {
+ const sev = f.severity || '—';
+ const cat = f.category || '—';
+ // Per-band copy — same data, different tone.
+ let title, subtitle, primary, secondary;
+ if (band === 'kid') {
+ title = `${emojiFor(f.severity, f.status)} ${f.processName}`;
+ subtitle = plainLabel(f);
+ primary = 'Okay, got it';
+ secondary = '';
+ } else if (band === 'senior' || band === 'mature') {
+ title = `${f.processName}`;
+ subtitle = `Severity: ${sev}. ${f.suggestedAction || ''}`;
+ primary = 'Mark resolved';
+ secondary = 'Close';
+ } else {
+ title = `${f.processName}`;
+ subtitle = `${f.source} · ${f.eventCount} events · severity ${sev} · ${cat}`;
+ primary = 'Mark resolved';
+ secondary = 'Open in CNCP';
+ }
+ return `
+ <h3>${title}</h3>
+ <div style="color:var(--muted); margin-bottom:14px;">${subtitle}</div>
+ <div class="modal-row"><div class="k">Process</div><div>${f.processName}</div></div>
+ <div class="modal-row adult-only"><div class="k">Source</div><div>${f.source}</div></div>
+ <div class="modal-row adult-only"><div class="k">Severity</div><div>${sev}</div></div>
+ <div class="modal-row adult-only"><div class="k">Category</div><div>${cat}</div></div>
+ <div class="modal-row adult-only"><div class="k">Events</div><div>${f.eventCount}</div></div>
+ <div class="modal-row"><div class="k">Action</div><div>${f.suggestedAction || '—'}</div></div>
+ <div class="modal-row adult-only"><div class="k">Last output</div><div style="font-family:var(--font-mono); font-size:0.9em; white-space:pre-wrap; max-height:140px; overflow:auto;">${(f.lastLines||'').slice(0,800)}</div></div>
+ <div class="modal-actions">
+ <button class="action-btn" onclick="markResolved('${f.id}')">${primary}</button>
+ ${secondary ? `<button class="action-btn" style="background:transparent; color:var(--fg); border:1px solid var(--border);" onclick="${secondary==='Close' ? 'closeModal()' : `window.open('http://127.0.0.1:3333/','_blank'); closeModal();`}">${secondary}</button>` : ''}
+ </div>`;
+}
+
+function markResolved(id) {
+ _localActions.set(id, { ...(_localActions.get(id)||{}), resolved: true });
+ closeModal();
+ const band = bandFor(Number(slider.value)).id;
+ const msg = band === 'kid' ? '✓ Marked all good!'
+ : band === 'toddler' ? '🎉 Yay!'
+ : band === 'elder' ? 'Done. Thank you.'
+ : 'Resolved · row will move to Resolved bucket';
+ toast(msg);
+ renderGrid(bandFor(Number(slider.value)));
+}
+
+function onAction(band, id, btn) {
+ const f = findFailure(id);
+ if (!f) { toast('item not found'); return; }
+ if (band === 'toddler') {
+ // celebrate on tap
+ _localActions.set(id, { ...(_localActions.get(id)||{}), celebrated: true });
+ openModal(`<div class="toddler-celebrate">🎉</div><div style="text-align:center; font-size:1.3em; font-weight:700; margin-top:14px;">Good job!</div><div class="modal-actions" style="justify-content:center;"><button class="action-btn" onclick="closeModal()">Okay!</button></div>`);
+ return;
+ }
+ if (band === 'elder') {
+ // confirm-resolve in one step
+ openModal(`
+ <h3 style="font-size:1.4em;">Mark as done?</h3>
+ <div style="margin:14px 0; font-size:1.15em;">${f.suggestedAction || f.processName}</div>
+ <div class="modal-actions">
+ <button class="action-btn" onclick="markResolved('${id}')">Yes, done</button>
+ <button class="action-btn" style="background:transparent; color:var(--fg); border:2px solid var(--border);" onclick="closeModal()">Not yet</button>
+ </div>`);
+ return;
+ }
+ // all other bands → detail modal
+ openModal(detailHTML(f, band));
+}
+
+// Re-render needs to respect _localActions so resolved items show as resolved.
+const _origRenderGrid = renderGrid;
+renderGrid = function(b) {
+ // mutate the cached data locally to reflect _localActions before rendering
+ if (_cachedData && Array.isArray(_cachedData.failures)) {
+ _cachedData.failures.forEach(f => {
+ const local = _localActions.get(f.id);
+ if (local && local.resolved && f.status === 'triage') f.status = 'resolved';
+ });
+ }
+ _origRenderGrid(b);
+};
+
loadData();
</script>
</body></html>
← 12220e8 v7 age-adaptive: 3→95 slider morphs UI through 8 design band
·
back to Cncp Failures Mockups
·
v7 voices: 8 per-band TTS profiles (voice/pitch/rate) via We 3ab046b →