From 8624d1887a7ce10a9619d1d9c9c7a6c33641d303 Mon Sep 17 00:00:00 2001 From: jae Date: Fri, 3 Apr 2026 00:16:27 +0000 Subject: [PATCH] feat: subcategories as 4-column card grid with expandable detail panel --- css/contraband.css | 123 +++++++++++++++++++++++++++++++++------------ js/contraband.js | 104 +++++++++++++++++++++----------------- 2 files changed, 148 insertions(+), 79 deletions(-) diff --git a/css/contraband.css b/css/contraband.css index cfba481..1b5b9da 100644 --- a/css/contraband.css +++ b/css/contraband.css @@ -268,66 +268,125 @@ letter-spacing: 1px; } -/* ─── Subcategory Sections ───────────────────────────── */ -.crt-subcategory { +/* ─── Subcategory Grid (4-col cards) ─────────────────── */ +.crt-sub-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; margin-bottom: 1.5rem; } -.crt-sub-header { +.crt-sub-card { + background: rgba(20, 20, 20, 0.9); + border: 1px solid var(--border); + border-left: 3px solid var(--status-amber); + padding: 1rem 1.2rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.crt-sub-card:hover { + background: rgba(30, 30, 30, 0.95); + border-left-color: var(--mil-red); + transform: translateY(-1px); +} + +.crt-sub-card.active { + background: rgba(35, 35, 35, 0.95); + border-color: var(--status-amber); + border-left-color: var(--status-amber); + box-shadow: 0 0 12px rgba(201, 162, 39, 0.15); +} + +.crt-sub-card-name { + font-family: var(--font-display); + font-size: 0.85rem; + font-weight: 600; + color: var(--text-primary); + letter-spacing: 0.5px; + line-height: 1.3; +} + +.crt-sub-card-stats { + display: flex; + gap: 0.75rem; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-secondary); + letter-spacing: 0.5px; +} + +.crt-sub-card-stats .amber { + color: var(--warning); +} + +/* ─── Subcategory Detail Panel ───────────────────────── */ +.crt-sub-detail { + margin-bottom: 2rem; + border: 1px solid var(--border); + border-top: 2px solid var(--status-amber); + background: rgba(16, 16, 16, 0.95); +} + +.crt-sub-detail-header { display: flex; align-items: center; gap: 0.75rem; - padding: 0.6rem 1rem; + padding: 0.8rem 1.2rem; background: rgba(20, 20, 20, 0.9); - border: 1px solid var(--border); - border-left: 2px solid var(--status-amber); - margin-bottom: 0.5rem; - cursor: pointer; - transition: all 0.2s ease; + border-bottom: 1px solid var(--border); } -.crt-sub-header:hover { - background: rgba(25, 25, 25, 0.9); - border-left-color: var(--mil-red); -} - -.crt-sub-toggle { - color: var(--text-muted); - font-size: 1rem; - transition: transform 0.3s ease; -} - -.crt-sub-header.collapsed .crt-sub-toggle { - transform: rotate(-90deg); -} - -.crt-sub-name { +.crt-sub-detail-name { font-family: var(--font-display); - font-size: 0.95rem; + font-size: 1rem; font-weight: 600; color: var(--text-primary); letter-spacing: 1px; flex: 1; } +.crt-sub-toggle { + color: var(--text-muted); + font-size: 1rem; +} + .crt-sub-count { font-family: var(--font-mono); - font-size: 0.95rem; + font-size: 0.85rem; color: var(--text-muted); letter-spacing: 1px; } -.crt-sub-notes { - padding: 0.5rem 1rem; - margin-bottom: 0.5rem; +.crt-sub-detail-notes { + padding: 0.6rem 1.2rem; font-family: var(--font-mono); - font-size: 1rem; + font-size: 0.9rem; color: var(--status-amber); line-height: 1.5; - border-left: 2px solid var(--status-amber); + border-bottom: 1px solid var(--border); background: rgba(201, 162, 39, 0.05); } +.crt-sub-detail-entries { + /* entries rendered inside here */ +} + +/* ─── Responsive subcategory grid ────────────────────── */ +@media (max-width: 1200px) { + .crt-sub-grid { grid-template-columns: repeat(3, 1fr); } +} +@media (max-width: 900px) { + .crt-sub-grid { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 600px) { + .crt-sub-grid { grid-template-columns: 1fr; } +} + + /* ─── Entry List ─────────────────────────────────────── */ .crt-entries { border: 1px solid var(--border); diff --git a/js/contraband.js b/js/contraband.js index eb014b9..1c62a6f 100644 --- a/js/contraband.js +++ b/js/contraband.js @@ -108,37 +108,29 @@ `; - // Subcategories - html += `
`; + // Subcategory grid (4-col cards) + html += `
`; for (let i = 0; i < cat.subcategories.length; i++) { const sub = cat.subcategories[i]; if (sub.entries.length === 0 && sub.notes.length === 0) continue; - - html += `
`; - html += ``; } html += `
`; + // Shared detail panel (populated on card click) + html += ``; + // End of grid root.innerHTML = html; @@ -250,25 +242,48 @@ loadIndex(); }); - // Subcategory toggle (collapse/expand) - document.querySelectorAll('.crt-sub-header').forEach(header => { - header.addEventListener('click', () => { - const idx = header.dataset.toggle; - const entries = document.getElementById(`crtEntries${idx}`); - if (!entries) return; - const isCollapsed = header.classList.contains('collapsed'); - const notes = document.getElementById(`crtNotes${idx}`); - if (isCollapsed) { - header.classList.remove('collapsed'); - entries.style.display = ''; - if (notes) notes.style.display = ''; - header.querySelector('.crt-sub-toggle').textContent = '▾'; - } else { - header.classList.add('collapsed'); - entries.style.display = 'none'; - if (notes) notes.style.display = 'none'; - header.querySelector('.crt-sub-toggle').textContent = '▸'; + // Subcategory card clicks + document.querySelectorAll('.crt-sub-card').forEach(card => { + card.addEventListener('click', () => { + const idx = parseInt(card.dataset.subIdx); + const sub = cat.subcategories[idx]; + const panel = document.getElementById('crtSubDetail'); + const headerEl = document.getElementById('crtSubDetailHeader'); + const notesEl = document.getElementById('crtSubDetailNotes'); + const entriesEl = document.getElementById('crtSubDetailEntries'); + + // If clicking the already active card, collapse + const wasActive = card.classList.contains('active'); + document.querySelectorAll('.crt-sub-card').forEach(c => c.classList.remove('active')); + + if (wasActive) { + panel.style.display = 'none'; + return; } + + card.classList.add('active'); + + // Populate header + headerEl.innerHTML = ` ${esc(sub.name)} ${sub.entries.length} items`; + + // Populate notes + if (sub.notes && sub.notes.length > 0) { + let nh = ''; + for (const note of sub.notes) nh += `⚠ ${esc(note)}
`; + notesEl.innerHTML = nh; + notesEl.style.display = ''; + } else { + notesEl.innerHTML = ''; + notesEl.style.display = 'none'; + } + + // Populate entries + let eh = ''; + for (const entry of sub.entries) eh += renderEntry(entry); + entriesEl.innerHTML = eh; + + panel.style.display = ''; + panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); @@ -301,11 +316,6 @@ const text = entry.textContent.toLowerCase(); entry.style.display = text.includes(q) ? '' : 'none'; }); - // Hide empty subcategories - document.querySelectorAll('.crt-subcategory').forEach(sub => { - const visible = sub.querySelectorAll('.crt-entry:not([style*="display: none"])'); - sub.style.display = visible.length > 0 || !q ? '' : 'none'; - }); }); } }