[object Object]

← back to Norma Sdcc Pitch

add: dashboard modals — click priority cards / loop cards / cron cards opens detail modal with extended sections; +3 XP per modal opened; Vault section deployed with full token form

c6eb87e3daf90adf790af54a17990806c45b3218 · 2026-05-20 10:04:42 -0700 · SteveStudio2

Files touched

Diff

commit c6eb87e3daf90adf790af54a17990806c45b3218
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 20 10:04:42 2026 -0700

    add: dashboard modals — click priority cards / loop cards / cron cards opens detail modal with extended sections; +3 XP per modal opened; Vault section deployed with full token form
---
 public/app.js     | 100 ++++++++++++++++++++++++++++++++++++++++++++++++------
 public/index.html |  14 ++++++++
 public/styles.css |  18 ++++++++++
 3 files changed, 122 insertions(+), 10 deletions(-)

diff --git a/public/app.js b/public/app.js
index 82b0df7..828d767 100644
--- a/public/app.js
+++ b/public/app.js
@@ -119,14 +119,44 @@
     d.innerHTML = p.docs_overview.map((x) => `<div class="card"><h4>${escapeHtml(x.title)}</h4><p>${escapeHtml(x.detail)}</p></div>`).join('');
   }
 
-  /* ─────────────── render: loops + cron cards ─────────────── */
+  /* ─────────────── render: loops + cron cards (clickable for detail modal) ─────────────── */
   function renderLoopsCards() {
     const c = document.getElementById('grid-loops');
-    c.innerHTML = state.data.lecture.loops.examples.map((x) => `<div class="card"><h4>${escapeHtml(x.title)}</h4><p>${escapeHtml(x.detail)}</p></div>`).join('');
+    const examples = state.data.lecture.loops.examples;
+    c.innerHTML = examples.map((x, i) => `<div class="card clickable-card" data-loop-idx="${i}"><h4>${escapeHtml(x.title)}</h4><p>${escapeHtml(x.detail)}</p><span class="card-cta-mini">Click for detail →</span></div>`).join('');
+    c.querySelectorAll('[data-loop-idx]').forEach((card) => {
+      card.addEventListener('click', () => {
+        const i = parseInt(card.getAttribute('data-loop-idx'), 10);
+        const x = examples[i];
+        openDetailModal({
+          tag: 'LOOP', title: x.title, tagline: 'Always-on background agent',
+          sections: [
+            { h: 'What it does', p: x.detail },
+            { h: 'Guardrails', list: state.data.lecture.loops.guardrails },
+            { h: 'How it fits the workflow', p: 'Triggered automatically on the listed cadence. Outputs land in Norma\'s queue, not in SDCC\'s direct outbox — staff approves before send. Every action is audited.' },
+          ],
+        });
+      });
+    });
   }
   function renderCronCards() {
     const c = document.getElementById('grid-cron');
-    c.innerHTML = state.data.lecture.cron.examples.map((x) => `<div class="card"><h4>${escapeHtml(x.title)}</h4><p>${escapeHtml(x.detail)}</p></div>`).join('');
+    const examples = state.data.lecture.cron.examples;
+    c.innerHTML = examples.map((x, i) => `<div class="card clickable-card" data-cron-idx="${i}"><h4>${escapeHtml(x.title)}</h4><p>${escapeHtml(x.detail)}</p><span class="card-cta-mini">Click for detail →</span></div>`).join('');
+    c.querySelectorAll('[data-cron-idx]').forEach((card) => {
+      card.addEventListener('click', () => {
+        const i = parseInt(card.getAttribute('data-cron-idx'), 10);
+        const x = examples[i];
+        openDetailModal({
+          tag: 'CRON', title: x.title, tagline: 'Scheduled task — calendar-driven',
+          sections: [
+            { h: 'What it does', p: x.detail },
+            { h: 'Loops vs Cron', p: state.data.lecture.cron.vs_loops },
+            { h: 'How SDCC will see it', p: 'Outputs land as a draft / brief / suggestion in Norma at the scheduled time. Staff reviews and approves. Configurable schedule per cron in Settings.' },
+          ],
+        });
+      });
+    });
   }
 
   /* ─────────────── render: UI vs UX ─────────────── */
@@ -246,6 +276,30 @@
     document.querySelectorAll('.modal').forEach((m) => (m.hidden = true));
     state.currentAgentId = null;
   }
+
+  /* generic detail modal — for priorities, loops, cron, anything */
+  function openDetailModal({ tag, title, tagline, sections }) {
+    const m = document.getElementById('modal-detail');
+    if (!m) return;
+    document.getElementById('detail-tag').textContent = tag || '';
+    document.getElementById('detail-title').textContent = title || '';
+    document.getElementById('detail-tagline').textContent = tagline || '';
+    const body = document.getElementById('detail-body');
+    body.innerHTML = (sections || []).map((s) => {
+      let inner = '';
+      if (s.p) inner += `<p>${escapeHtml(s.p)}</p>`;
+      if (s.list) inner += `<ul class="lecture-bullets compact" style="margin-top:8px">${s.list.map((x) => `<li>${escapeHtml(x)}</li>`).join('')}</ul>`;
+      if (s.beforeAfter) inner += `
+        <div class="priority-before-after" style="margin:8px 0">
+          <div class="ba-cell before"><span class="ba-tag">Before</span>${escapeHtml(s.beforeAfter.before)}</div>
+          <div class="ba-cell after"><span class="ba-tag">After</span>${escapeHtml(s.beforeAfter.after)}</div>
+        </div>`;
+      if (s.pills) inner += `<div class="priority-agents" style="margin-top:6px">${s.pills.map((p) => `<span class="agent-pill">${escapeHtml(p)}</span>`).join('')}</div>`;
+      return `<section><h3>${escapeHtml(s.h)}</h3>${inner}</section>`;
+    }).join('');
+    m.hidden = false;
+    addXp(3, 'Opened detail modal');
+  }
   function wireModalCloseHandlers() {
     document.querySelectorAll('[data-close]').forEach((el) => el.addEventListener('click', closeModal));
     document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
@@ -551,33 +605,59 @@ ${items.map((a) => `
     if (!p) return;
     const list = document.getElementById('priority-list');
     if (!list) return;
-    list.innerHTML = p.items.map((it) => `
-      <article class="priority-card">
+    list.innerHTML = p.items.map((it, i) => `
+      <article class="priority-card clickable-card" data-priority-idx="${i}">
         <div class="priority-rank">${it.rank}</div>
         <div class="priority-body">
           <span class="priority-icon-tag">${escapeHtml(it.icon || '')}</span>
           <h3>${escapeHtml(it.title)}</h3>
           <p class="pain"><span class="priority-section-label">Pain</span>${escapeHtml(it.the_pain)}</p>
           <p class="solution"><span class="priority-section-label">Solution</span>${escapeHtml(it.the_solution)}</p>
-          ${it.how_norma_helps ? `<ul class="priority-howlist">${it.how_norma_helps.map((h) => `<li>${escapeHtml(h)}</li>`).join('')}</ul>` : ''}
-          ${it.wix_specific_steps ? `<ul class="priority-howlist">${it.wix_specific_steps.map((h) => `<li>${escapeHtml(h)}</li>`).join('')}</ul>` : ''}
           ${it.before_after ? `
             <div class="priority-before-after">
               <div class="ba-cell before"><span class="ba-tag">Before</span>${escapeHtml(it.before_after.before)}</div>
               <div class="ba-cell after"><span class="ba-tag">After</span>${escapeHtml(it.before_after.after)}</div>
             </div>` : ''}
-          ${it.agents_skills ? `<div class="priority-agents">${it.agents_skills.map((a) => `<span class="agent-pill">${escapeHtml(a)}</span>`).join('')}</div>` : ''}
+          <div class="card-cta-row">
+            <span class="card-cta">Click for full breakdown →</span>
+            ${it.agents_skills ? `<div class="priority-agents">${it.agents_skills.map((a) => `<span class="agent-pill" data-pill="${escapeHtml(a)}">${escapeHtml(a)}</span>`).join('')}</div>` : ''}
+          </div>
         </div>
       </article>
     `).join('');
-    // wire agent pills to scroll to agent grid
+    list.querySelectorAll('.priority-card').forEach((card) => {
+      card.addEventListener('click', (e) => {
+        // Skip if click hit an agent pill
+        if (e.target.closest('.agent-pill')) return;
+        openPriorityModal(parseInt(card.getAttribute('data-priority-idx'), 10));
+      });
+    });
     list.querySelectorAll('.agent-pill').forEach((p) => {
-      p.addEventListener('click', () => {
+      p.addEventListener('click', (e) => {
+        e.stopPropagation();
         document.querySelector('#agents').scrollIntoView({ behavior: 'smooth' });
       });
     });
   }
 
+  function openPriorityModal(idx) {
+    const it = state.data.priorities.items[idx];
+    if (!it) return;
+    openDetailModal({
+      tag: `Priority ${it.rank}`,
+      title: it.icon + ' ' + it.title,
+      tagline: '',
+      sections: [
+        { h: 'The pain', p: it.the_pain },
+        { h: 'The solution', p: it.the_solution },
+        ...(it.how_norma_helps ? [{ h: 'How Norma helps (4 specific moves)', list: it.how_norma_helps }] : []),
+        ...(it.wix_specific_steps ? [{ h: 'Wix-specific implementation steps', list: it.wix_specific_steps }] : []),
+        ...(it.before_after ? [{ h: 'Before / after', beforeAfter: it.before_after }] : []),
+        ...(it.agents_skills ? [{ h: 'Agents + skills involved', pills: it.agents_skills }] : []),
+      ],
+    });
+  }
+
   /* ─────────────── reluctant adopter objections ─────────────── */
   function renderObjections() {
     const ra = state.data.reluctant_adopter;
diff --git a/public/index.html b/public/index.html
index c3ca7bf..2684fcf 100644
--- a/public/index.html
+++ b/public/index.html
@@ -467,6 +467,20 @@
   </div>
 </div>
 
+<!-- ─────────── generic detail modal (priority/loop/cron/etc) ─────────── -->
+<div class="modal" id="modal-detail" hidden>
+  <div class="modal-backdrop" data-close></div>
+  <div class="modal-card wide" role="dialog">
+    <button class="modal-close" data-close>×</button>
+    <div class="modal-header">
+      <span class="badge" id="detail-tag"></span>
+      <h2 id="detail-title"></h2>
+      <p class="tagline" id="detail-tagline"></p>
+    </div>
+    <div class="modal-body" id="detail-body"></div>
+  </div>
+</div>
+
 <!-- ─────────── toast / xp gain ─────────── -->
 <div class="toast-stack" id="toast-stack"></div>
 
diff --git a/public/styles.css b/public/styles.css
index f5bde3a..329d5f5 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -612,6 +612,24 @@ code { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: .85em; bac
   font-size: 12px; font-weight: 500; cursor: pointer;
 }
 .priority-agents .agent-pill:hover { background: var(--info); color: #fff; }
+.clickable-card { cursor: pointer; transition: all .15s; }
+.clickable-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
+.card-cta-row {
+  display: flex; justify-content: space-between; align-items: center;
+  margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--border-soft);
+  gap: 12px; flex-wrap: wrap;
+}
+.card-cta {
+  color: var(--sdcc-blue); font-size: 13px; font-weight: 600;
+  display: inline-flex; align-items: center; gap: 4px;
+}
+.card-cta-mini {
+  display: block; margin-top: 8px; color: var(--sdcc-blue);
+  font-size: 12px; font-weight: 600;
+}
+.grid-cards .card.clickable-card { position: relative; }
+.grid-cards .card.clickable-card .card-cta-mini { opacity: .6; }
+.grid-cards .card.clickable-card:hover .card-cta-mini { opacity: 1; }
 @media (max-width: 720px) {
   .priority-card { grid-template-columns: 1fr; }
   .priority-rank { width: 56px; height: 56px; font-size: 24px; }

← 948a924 add: section 11 Token Vault — 6 categories (AI / Email / Adv  ·  back to Norma Sdcc Pitch  ·  feat(auth): rate-limit POST /login on norma-sdcc-pitch (10/1 418a7c6 →