feat: subcategories as 4-column card grid with expandable detail panel
This commit is contained in:
parent
2b0f76aa1f
commit
8624d1887a
2 changed files with 148 additions and 79 deletions
|
|
@ -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);
|
||||
|
|
|
|||
104
js/contraband.js
104
js/contraband.js
|
|
@ -108,37 +108,29 @@
|
|||
<button class="crt-filter-btn" data-filter="starred">⭐ STARRED (${cat.starred_count})</button>
|
||||
</div>`;
|
||||
|
||||
// Subcategories
|
||||
html += `<div id="crtSubcategories">`;
|
||||
// Subcategory grid (4-col cards)
|
||||
html += `<div class="crt-sub-grid" id="crtSubGrid">`;
|
||||
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 += `<div class="crt-subcategory" data-sub-idx="${i}">`;
|
||||
html += `<div class="crt-sub-header collapsed" data-toggle="${i}">
|
||||
<span class="crt-sub-toggle">▸</span>
|
||||
<span class="crt-sub-name">${esc(sub.name)}</span>
|
||||
<span class="crt-sub-count">${sub.entries.length} items</span>
|
||||
const starCount = sub.entries.filter(e => e.starred).length;
|
||||
html += `<div class="crt-sub-card" data-sub-idx="${i}">
|
||||
<div class="crt-sub-card-name">${esc(sub.name)}</div>
|
||||
<div class="crt-sub-card-stats">
|
||||
<span>${sub.entries.length} items</span>
|
||||
${starCount > 0 ? `<span class="amber">⭐ ${starCount}</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Notes
|
||||
if (sub.notes && sub.notes.length > 0) {
|
||||
html += `<div class="crt-sub-notes" style="display:none" id="crtNotes${i}">`;
|
||||
for (const note of sub.notes) {
|
||||
html += `⚠ ${esc(note)}<br>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
// Entries
|
||||
html += `<div class="crt-entries" id="crtEntries${i}" style="display:none">`;
|
||||
for (const entry of sub.entries) {
|
||||
html += renderEntry(entry);
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
// Shared detail panel (populated on card click)
|
||||
html += `<div class="crt-sub-detail" id="crtSubDetail" style="display:none">
|
||||
<div class="crt-sub-detail-header" id="crtSubDetailHeader"></div>
|
||||
<div class="crt-sub-detail-notes" id="crtSubDetailNotes"></div>
|
||||
<div class="crt-sub-detail-entries" id="crtSubDetailEntries"></div>
|
||||
</div>`;
|
||||
|
||||
// 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 = `<span class="crt-sub-toggle">▾</span> <span class="crt-sub-detail-name">${esc(sub.name)}</span> <span class="crt-sub-count">${sub.entries.length} items</span>`;
|
||||
|
||||
// Populate notes
|
||||
if (sub.notes && sub.notes.length > 0) {
|
||||
let nh = '';
|
||||
for (const note of sub.notes) nh += `⚠ ${esc(note)}<br>`;
|
||||
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';
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue