[object Object]

← 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

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 →