feat(ui): inline expansion for mobile stream entries
Replace bottom-sheet peek with inline accordion at ≤900px. Entries expand in-place with grid-template-rows animation (0.2s). Body clamped to 3 lines; fullscreen uncaps it. Selection toggles DOM classes instead of re-rendering for fluid j/k nav.
This commit is contained in:
+81
-15
@@ -648,6 +648,30 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderInlineDetail(e) {
|
||||
const tags = (e.tags || []).map(t => `<span class="detail-tag">#${t}</span>`).join('');
|
||||
let actions = '';
|
||||
actions += `<button class="action-btn" onclick="event.stopPropagation();nibApp.enterMode('edit')">edit</button>`;
|
||||
if (!e.card_type) {
|
||||
actions += `<button class="action-btn" onclick="event.stopPropagation();nibApp.showAbsorb('${e.id}')">absorb</button>`;
|
||||
actions += `<button class="action-btn primary" onclick="event.stopPropagation();nibApp.showPromote('${e.id}')">promote</button>`;
|
||||
}
|
||||
if (e.card_type) {
|
||||
actions += `<button class="action-btn danger" onclick="event.stopPropagation();nibApp.demoteEntity('${e.id}')">demote</button>`;
|
||||
} else {
|
||||
actions += `<button class="action-btn danger" onclick="event.stopPropagation();nibApp.deleteEntity('${e.id}')">delete</button>`;
|
||||
}
|
||||
return `<div class="exp-inner">
|
||||
<div class="exp-body">${escHtml(e.body || '')}</div>
|
||||
${tags ? `<div class="exp-tags">${tags}</div>` : ''}
|
||||
<div class="exp-acts">${actions}</div>
|
||||
<div class="exp-toolbar">
|
||||
<button class="peek-mobile-btn" onclick="event.stopPropagation();nibApp.expandInline()">↑</button>
|
||||
<button class="peek-mobile-btn" onclick="event.stopPropagation();nibApp.dismissPeek()">×</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderEntityItem(e, idx) {
|
||||
const glyph = displayGlyph(e);
|
||||
const gc = glyphClass(e);
|
||||
@@ -668,11 +692,18 @@
|
||||
}
|
||||
|
||||
return `<div class="entity-item${selected}${isCard}" data-index="${idx}" data-id="${e.id}">
|
||||
<span class="entity-glyph ${gc}">${glyph}</span>
|
||||
${label}
|
||||
${time}
|
||||
<span class="entity-tags">${tags}${cardBadge}</span>
|
||||
<span class="entity-meta">${useBadge}</span>
|
||||
<div class="entity-head">
|
||||
<span class="entity-glyph ${gc}">${glyph}</span>
|
||||
${label}
|
||||
${time}
|
||||
<span class="entity-tags">${tags}${cardBadge}</span>
|
||||
<span class="entity-meta">${useBadge}</span>
|
||||
</div>
|
||||
<div class="entity-exp">
|
||||
<div class="entity-exp-clip">
|
||||
${renderInlineDetail(e)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -694,21 +725,16 @@
|
||||
|
||||
pane.classList.add('visible');
|
||||
|
||||
const mobileBar = `<div class="peek-mobile-bar">
|
||||
<button class="peek-mobile-btn" onclick="nibApp.togglePeekFull()">↑</button>
|
||||
<button class="peek-mobile-btn" onclick="nibApp.dismissPeek()">×</button>
|
||||
</div>`;
|
||||
|
||||
if (state.peekMode === 'edit') {
|
||||
pane.innerHTML = mobileBar + renderEditMode(e);
|
||||
pane.innerHTML = renderEditMode(e);
|
||||
} else if (state.view === 'stream' || !e.card_type) {
|
||||
pane.innerHTML = mobileBar + renderStreamPeek(e);
|
||||
pane.innerHTML = renderStreamPeek(e);
|
||||
} else if (state.peekMode === 'run') {
|
||||
pane.innerHTML = mobileBar + renderRunMode(e);
|
||||
pane.innerHTML = renderRunMode(e);
|
||||
} else if (state.peekMode === 'fill') {
|
||||
pane.innerHTML = mobileBar + renderFillMode(e);
|
||||
pane.innerHTML = renderFillMode(e);
|
||||
} else {
|
||||
pane.innerHTML = mobileBar + renderCardPeek(e);
|
||||
pane.innerHTML = renderCardPeek(e);
|
||||
}
|
||||
|
||||
bindPeekEvents(e);
|
||||
@@ -1105,6 +1131,20 @@
|
||||
// ========== Actions ==========
|
||||
|
||||
function selectEntity(idx) {
|
||||
if (isMobileBreakpoint()) {
|
||||
const prev = state.selectedIndex;
|
||||
if (prev === idx) {
|
||||
state.selectedIndex = -1;
|
||||
} else {
|
||||
state.selectedIndex = idx;
|
||||
}
|
||||
$$('.entity-item.selected').forEach(el => el.classList.remove('selected'));
|
||||
if (state.selectedIndex >= 0) {
|
||||
const target = $(`.entity-item[data-index="${state.selectedIndex}"]`);
|
||||
if (target) target.classList.add('selected');
|
||||
}
|
||||
return;
|
||||
}
|
||||
state.selectedIndex = idx;
|
||||
state.peekMode = 'preview';
|
||||
state.runChecked = new Set();
|
||||
@@ -1408,13 +1448,31 @@
|
||||
},
|
||||
|
||||
togglePeekFull() {
|
||||
if (isMobileBreakpoint()) {
|
||||
this.expandInline();
|
||||
return;
|
||||
}
|
||||
const pane = $('#detail-pane');
|
||||
pane.classList.toggle('peek-full');
|
||||
const btn = pane.querySelector('.peek-mobile-btn');
|
||||
if (btn) btn.textContent = pane.classList.contains('peek-full') ? '↓' : '↑';
|
||||
},
|
||||
|
||||
expandInline() {
|
||||
const sel = $(`.entity-item.selected`);
|
||||
if (!sel) return;
|
||||
sel.classList.toggle('exp-full');
|
||||
const btn = sel.querySelector('.exp-toolbar .peek-mobile-btn');
|
||||
if (btn) btn.textContent = sel.classList.contains('exp-full') ? '↓' : '↑';
|
||||
},
|
||||
|
||||
dismissPeek() {
|
||||
if (isMobileBreakpoint()) {
|
||||
const sel = $(`.entity-item.selected`);
|
||||
if (sel) sel.classList.remove('selected', 'exp-full');
|
||||
state.selectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
const pane = $('#detail-pane');
|
||||
pane.classList.remove('visible', 'peek-full');
|
||||
state.selectedIndex = -1;
|
||||
@@ -1471,6 +1529,14 @@
|
||||
}
|
||||
|
||||
if (ev.key === 'Escape') {
|
||||
if (isMobileBreakpoint()) {
|
||||
if (state.selectedIndex >= 0) {
|
||||
const sel = $(`.entity-item.selected`);
|
||||
if (sel) sel.classList.remove('selected', 'exp-full');
|
||||
state.selectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const pane = $('#detail-pane');
|
||||
if ($('main').classList.contains('focus-peek')) {
|
||||
exitFocusPeek();
|
||||
|
||||
Reference in New Issue
Block a user