feat: subcategories as 4-column card grid with expandable detail panel

This commit is contained in:
jae 2026-04-03 00:16:27 +00:00
parent 2b0f76aa1f
commit 8624d1887a
2 changed files with 148 additions and 79 deletions

View file

@ -268,66 +268,125 @@
letter-spacing: 1px; letter-spacing: 1px;
} }
/* ─── Subcategory Sections ───────────────────────────── */ /* ─── Subcategory Grid (4-col cards) ─────────────────── */
.crt-subcategory { .crt-sub-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-bottom: 1.5rem; 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; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.6rem 1rem; padding: 0.8rem 1.2rem;
background: rgba(20, 20, 20, 0.9); background: rgba(20, 20, 20, 0.9);
border: 1px solid var(--border); border-bottom: 1px solid var(--border);
border-left: 2px solid var(--status-amber);
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
} }
.crt-sub-header:hover { .crt-sub-detail-name {
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 {
font-family: var(--font-display); font-family: var(--font-display);
font-size: 0.95rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
letter-spacing: 1px; letter-spacing: 1px;
flex: 1; flex: 1;
} }
.crt-sub-toggle {
color: var(--text-muted);
font-size: 1rem;
}
.crt-sub-count { .crt-sub-count {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.95rem; font-size: 0.85rem;
color: var(--text-muted); color: var(--text-muted);
letter-spacing: 1px; letter-spacing: 1px;
} }
.crt-sub-notes { .crt-sub-detail-notes {
padding: 0.5rem 1rem; padding: 0.6rem 1.2rem;
margin-bottom: 0.5rem;
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 1rem; font-size: 0.9rem;
color: var(--status-amber); color: var(--status-amber);
line-height: 1.5; line-height: 1.5;
border-left: 2px solid var(--status-amber); border-bottom: 1px solid var(--border);
background: rgba(201, 162, 39, 0.05); 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 ─────────────────────────────────────── */ /* ─── Entry List ─────────────────────────────────────── */
.crt-entries { .crt-entries {
border: 1px solid var(--border); border: 1px solid var(--border);

View file

@ -108,36 +108,28 @@
<button class="crt-filter-btn" data-filter="starred"> STARRED (${cat.starred_count})</button> <button class="crt-filter-btn" data-filter="starred"> STARRED (${cat.starred_count})</button>
</div>`; </div>`;
// Subcategories // Subcategory grid (4-col cards)
html += `<div id="crtSubcategories">`; html += `<div class="crt-sub-grid" id="crtSubGrid">`;
for (let i = 0; i < cat.subcategories.length; i++) { for (let i = 0; i < cat.subcategories.length; i++) {
const sub = cat.subcategories[i]; const sub = cat.subcategories[i];
if (sub.entries.length === 0 && sub.notes.length === 0) continue; if (sub.entries.length === 0 && sub.notes.length === 0) continue;
const starCount = sub.entries.filter(e => e.starred).length;
html += `<div class="crt-subcategory" data-sub-idx="${i}">`; html += `<div class="crt-sub-card" data-sub-idx="${i}">
html += `<div class="crt-sub-header collapsed" data-toggle="${i}"> <div class="crt-sub-card-name">${esc(sub.name)}</div>
<span class="crt-sub-toggle"></span> <div class="crt-sub-card-stats">
<span class="crt-sub-name">${esc(sub.name)}</span> <span>${sub.entries.length} items</span>
<span class="crt-sub-count">${sub.entries.length} items</span> ${starCount > 0 ? `<span class="amber">⭐ ${starCount}</span>` : ''}
</div>
</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>`; html += `</div>`;
}
// Entries // Shared detail panel (populated on card click)
html += `<div class="crt-entries" id="crtEntries${i}" style="display:none">`; html += `<div class="crt-sub-detail" id="crtSubDetail" style="display:none">
for (const entry of sub.entries) { <div class="crt-sub-detail-header" id="crtSubDetailHeader"></div>
html += renderEntry(entry); <div class="crt-sub-detail-notes" id="crtSubDetailNotes"></div>
} <div class="crt-sub-detail-entries" id="crtSubDetailEntries"></div>
html += `</div></div>`; </div>`;
}
html += `</div>`;
// End of grid // End of grid
@ -250,25 +242,48 @@
loadIndex(); loadIndex();
}); });
// Subcategory toggle (collapse/expand) // Subcategory card clicks
document.querySelectorAll('.crt-sub-header').forEach(header => { document.querySelectorAll('.crt-sub-card').forEach(card => {
header.addEventListener('click', () => { card.addEventListener('click', () => {
const idx = header.dataset.toggle; const idx = parseInt(card.dataset.subIdx);
const entries = document.getElementById(`crtEntries${idx}`); const sub = cat.subcategories[idx];
if (!entries) return; const panel = document.getElementById('crtSubDetail');
const isCollapsed = header.classList.contains('collapsed'); const headerEl = document.getElementById('crtSubDetailHeader');
const notes = document.getElementById(`crtNotes${idx}`); const notesEl = document.getElementById('crtSubDetailNotes');
if (isCollapsed) { const entriesEl = document.getElementById('crtSubDetailEntries');
header.classList.remove('collapsed');
entries.style.display = ''; // If clicking the already active card, collapse
if (notes) notes.style.display = ''; const wasActive = card.classList.contains('active');
header.querySelector('.crt-sub-toggle').textContent = '▾'; document.querySelectorAll('.crt-sub-card').forEach(c => c.classList.remove('active'));
} else {
header.classList.add('collapsed'); if (wasActive) {
entries.style.display = 'none'; panel.style.display = 'none';
if (notes) notes.style.display = 'none'; return;
header.querySelector('.crt-sub-toggle').textContent = '▸';
} }
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(); const text = entry.textContent.toLowerCase();
entry.style.display = text.includes(q) ? '' : 'none'; 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';
});
}); });
} }
} }