← 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
M public/app.jsM public/index.htmlM public/styles.css
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 →