bak
This commit is contained in:
1 parent
03f57cbaee
commit
a9603aa0e8
11 files changed
+1010
-93
No files matched your search
+195
-5
@@ -1,8 +1,8 @@
|
||||
// ── Detail view ──
|
||||
|
||||
import { $, esc, api } from './util.js';
|
||||
import { $, esc, api, fuzzyMatch } from './util.js';
|
||||
import { state, getChildren, getGroupCards, getRecent, markRecent } from './state.js';
|
||||
import { renderMd, firstImage } from './markdown.js';
|
||||
import { renderMd, stripMd, firstImage, addCodeCopyButtons } from './markdown.js';
|
||||
import { pushCardDelete, pushCardEdit, snapshotCard } from './undo.js';
|
||||
import { showDialog } from './dialog.js';
|
||||
|
||||
@@ -270,7 +270,15 @@ function startCloseMorph(cardId) {
|
||||
const target = resolveMorphTarget(cardId);
|
||||
if (!panel || !target) return;
|
||||
|
||||
const from = panel.getBoundingClientRect();
|
||||
// Use known inset values instead of getBoundingClientRect to avoid offset issues
|
||||
const isZen = detail.classList.contains('zen');
|
||||
const inset = isZen ? 0 : 40;
|
||||
const from = {
|
||||
left: inset,
|
||||
top: inset,
|
||||
width: window.innerWidth - inset * 2,
|
||||
height: window.innerHeight - inset * 2,
|
||||
};
|
||||
const to = target.getBoundingClientRect();
|
||||
if (from.width < 40 || from.height < 40 || to.width < 20 || to.height < 20) return;
|
||||
|
||||
@@ -286,19 +294,21 @@ function startCloseMorph(cardId) {
|
||||
? state.cards.find(c => c.id == target.dataset.id)
|
||||
: card;
|
||||
Object.assign(frame.style, {
|
||||
position: 'fixed',
|
||||
left: `${from.left}px`,
|
||||
top: `${from.top}px`,
|
||||
width: `${from.width}px`,
|
||||
height: `${from.height}px`,
|
||||
borderColor: targetCard?.color || card?.color || 'var(--t-accent2)',
|
||||
position: 'fixed',
|
||||
inset: 'auto',
|
||||
margin: '0',
|
||||
});
|
||||
|
||||
document.body.appendChild(frame);
|
||||
morphGhost = frame;
|
||||
|
||||
// Force reflow so the browser commits the starting position before transitioning
|
||||
void frame.offsetWidth;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
frame.style.left = `${to.left}px`;
|
||||
frame.style.top = `${to.top}px`;
|
||||
@@ -331,6 +341,7 @@ export function closeDetail(options = {}) {
|
||||
detail.classList.remove('open', 'zen');
|
||||
document.documentElement.classList.remove('detail-zen-open');
|
||||
document.body.classList.remove('detail-zen-open');
|
||||
$('split-right')?.classList.add('hidden');
|
||||
const icon = $('detail-zen')?.querySelector('i');
|
||||
if (icon) icon.className = 'ph ph-corners-out text-sm';
|
||||
state.viewingCard = null;
|
||||
@@ -400,6 +411,7 @@ function showContent(card) {
|
||||
$('detail-color').style.background = card.color;
|
||||
titleEl.textContent = card.title;
|
||||
bodyEl.innerHTML = renderMd(card.body);
|
||||
addCodeCopyButtons(bodyEl);
|
||||
applyReadingResizeHandles(bodyEl);
|
||||
bindDetailImageFullscreen(bodyEl);
|
||||
|
||||
@@ -522,6 +534,23 @@ document.addEventListener('keydown', e => {
|
||||
}, true);
|
||||
|
||||
// Zen mode
|
||||
// Tree view toggle
|
||||
// Extract selection to new card
|
||||
$('detail-extract')?.addEventListener('click', async () => {
|
||||
const sel = window.getSelection();
|
||||
const text = sel?.toString();
|
||||
if (!text?.trim()) return;
|
||||
await window._extractToCard(text);
|
||||
});
|
||||
|
||||
$('detail-tree')?.addEventListener('click', () => {
|
||||
const body = $('detail-body');
|
||||
if (!body) return;
|
||||
body.classList.toggle('tree');
|
||||
$('detail-tree')?.classList.toggle('text-t-accent', body.classList.contains('tree'));
|
||||
$('detail-tree')?.classList.toggle('border-t-accent', body.classList.contains('tree'));
|
||||
});
|
||||
|
||||
$('detail-zen')?.addEventListener('click', () => {
|
||||
detail.classList.toggle('zen');
|
||||
const isZen = detail.classList.contains('zen');
|
||||
@@ -531,6 +560,167 @@ $('detail-zen')?.addEventListener('click', () => {
|
||||
if (icon) icon.className = isZen ? 'ph ph-corners-in text-sm' : 'ph ph-corners-out text-sm';
|
||||
});
|
||||
|
||||
// ── Split pane (right) ──
|
||||
|
||||
const splitPane = $('split-right');
|
||||
const splitSearch = $('split-search');
|
||||
const splitPicks = $('split-picks');
|
||||
const splitContent = $('split-content');
|
||||
|
||||
function findRelatedCards(card) {
|
||||
if (!card) return [];
|
||||
const id = card.id;
|
||||
const title = (card.title || '').toLowerCase();
|
||||
const body = (card.body || '').toLowerCase();
|
||||
|
||||
// Extract significant words (3+ chars) from title and body
|
||||
const stopwords = new Set(['the','and','for','that','this','with','from','are','was','were','been','have','has','had','not','but','what','all','can','her','his','how','its','may','new','now','old','see','way','who','did','get','let','say','she','too','use','eine','ein','der','die','das','und','ist','von','mit','den','dem','des','auf','fuer','auch','sich','noch','wie','man','aber','nach','bei','nur','dann','wenn','oder','als','bis','zum','zur','kann','doch','ich','wir','sie','aus','vor']);
|
||||
const words = `${card.title} ${stripMd(card.body)}`.toLowerCase()
|
||||
.split(/[^a-zA-Z0-9äöüß]+/)
|
||||
.filter(w => w.length >= 3 && !stopwords.has(w));
|
||||
const wordSet = [...new Set(words)];
|
||||
|
||||
return state.cards
|
||||
.filter(c => c.id != id)
|
||||
.map(c => {
|
||||
let score = 0;
|
||||
const ct = (c.title || '').toLowerCase();
|
||||
const cb = stripMd(c.body || '').toLowerCase();
|
||||
|
||||
// Backlink: this card references the other
|
||||
if (body.includes(`[[${c.title.toLowerCase()}]]`)) score += 80;
|
||||
// Backlink: the other references this card
|
||||
if ((c.body || '').toLowerCase().includes(`[[${title}]]`)) score += 80;
|
||||
// Same bucket
|
||||
if (card.bucket_id && c.bucket_id == card.bucket_id) score += 15;
|
||||
// Same group
|
||||
if (card.parent_id && c.parent_id == card.parent_id) score += 20;
|
||||
|
||||
// Word overlap
|
||||
for (const w of wordSet) {
|
||||
if (ct.includes(w)) score += 6;
|
||||
if (cb.includes(w)) score += 2;
|
||||
}
|
||||
|
||||
return score > 0 ? { card: c, score } : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 12)
|
||||
.map(s => s.card);
|
||||
}
|
||||
|
||||
function renderSplitPicks(cards, label) {
|
||||
if (!cards.length) {
|
||||
splitPicks.innerHTML = `<div class="px-3 py-4 text-center text-t-dim font-pixel text-[9px]">no matches</div>`;
|
||||
return;
|
||||
}
|
||||
splitPicks.innerHTML =
|
||||
`<div class="px-3 pt-2 pb-1"><span class="font-pixel text-[9px] text-t-dim uppercase tracking-wider">${label}</span></div>` +
|
||||
cards.map(c => `<div class="split-pick" data-split-card="${c.id}">
|
||||
<span class="sp-dot" style="background:${c.color}"></span>
|
||||
<span class="font-pixel text-[10px] truncate flex-1">${esc(c.title)}</span>
|
||||
${c.bucket_name ? `<span class="text-[8px] text-t-dim font-pixel">${esc(c.bucket_name)}</span>` : ''}
|
||||
</div>`).join('');
|
||||
|
||||
splitPicks.querySelectorAll('[data-split-card]').forEach(el => {
|
||||
el.addEventListener('click', () => loadSplitCard(el.dataset.splitCard));
|
||||
});
|
||||
}
|
||||
|
||||
function searchAllCards(query) {
|
||||
const q = query.toLowerCase().trim();
|
||||
return state.cards
|
||||
.filter(c => c.id != state.viewingCard?.id)
|
||||
.map(c => {
|
||||
const t = (c.title || '').toLowerCase();
|
||||
const b = stripMd(c.body || '').toLowerCase();
|
||||
let score = 0;
|
||||
if (t.includes(q)) score += 50;
|
||||
if (t.startsWith(q)) score += 20;
|
||||
if (b.includes(q)) score += 10;
|
||||
// fuzzy on title
|
||||
const fm = fuzzyMatch(q, t);
|
||||
if (fm !== null) score += 5 + fm;
|
||||
return score > 0 ? { card: c, score } : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 15)
|
||||
.map(s => s.card);
|
||||
}
|
||||
|
||||
let splitCardId = null;
|
||||
|
||||
function loadSplitCard(id) {
|
||||
const card = state.cards.find(c => c.id == id);
|
||||
if (!card) return;
|
||||
splitCardId = id;
|
||||
splitPicks.innerHTML = `<div class="flex items-center border-b border-t-border">
|
||||
<button id="split-back" class="flex items-center gap-1.5 flex-1 px-3 py-2 text-xs text-t-muted hover:text-t-accent hover:bg-t-raised transition-colors">
|
||||
<i class="ph ph-arrow-left text-sm"></i><span class="font-pixel text-[10px]">related cards</span>
|
||||
</button>
|
||||
<button id="split-promote" class="flex items-center gap-1.5 px-3 py-2 text-xs text-t-muted hover:text-t-accent hover:bg-t-raised transition-colors" title="open as main card">
|
||||
<i class="ph ph-arrow-line-left text-sm"></i><span class="font-pixel text-[10px]">focus</span>
|
||||
</button>
|
||||
</div>`;
|
||||
splitPicks.querySelector('#split-back').addEventListener('click', openSplitPane);
|
||||
splitPicks.querySelector('#split-promote').addEventListener('click', () => promoteSplitCard(id));
|
||||
splitSearch.value = '';
|
||||
splitContent.innerHTML = `
|
||||
<div class="card-bar w-16 mb-3 rounded-sm" style="background:${card.color}"></div>
|
||||
<h2 class="font-pixel text-t-bright text-sm mb-3 leading-relaxed">${esc(card.title)}</h2>
|
||||
<div class="prose text-xs text-t-text">${renderMd(card.body)}</div>
|
||||
<div class="mt-6 pt-3 border-t border-t-border text-[10px] text-t-dim font-pixel">
|
||||
> ${card.created_at}${card.bucket_name ? ' | ' + esc(card.bucket_name) : ''}
|
||||
</div>`;
|
||||
splitContent.querySelectorAll('.card-link[data-card-id]').forEach(link => {
|
||||
link.addEventListener('click', () => loadSplitCard(link.dataset.cardId));
|
||||
});
|
||||
addCodeCopyButtons(splitContent);
|
||||
}
|
||||
|
||||
function promoteSplitCard(id) {
|
||||
// Open the split card as the main detail card, then refresh related in split pane
|
||||
openDetail(id);
|
||||
openSplitPane();
|
||||
}
|
||||
|
||||
function openSplitPane() {
|
||||
if (!splitPane) return;
|
||||
splitPane.classList.remove('hidden');
|
||||
splitSearch.value = '';
|
||||
splitContent.innerHTML = '';
|
||||
const related = findRelatedCards(state.viewingCard);
|
||||
renderSplitPicks(related, related.length ? 'related' : 'no related cards');
|
||||
splitSearch.focus();
|
||||
}
|
||||
|
||||
$('split-right-btn')?.addEventListener('click', () => {
|
||||
if (splitPane.classList.contains('hidden')) openSplitPane();
|
||||
});
|
||||
|
||||
$('split-close')?.addEventListener('click', () => splitPane.classList.add('hidden'));
|
||||
|
||||
splitSearch?.addEventListener('input', () => {
|
||||
const q = splitSearch.value.trim();
|
||||
if (!q) {
|
||||
const related = findRelatedCards(state.viewingCard);
|
||||
renderSplitPicks(related, 'related');
|
||||
return;
|
||||
}
|
||||
const results = searchAllCards(q);
|
||||
renderSplitPicks(results, `${results.length} result${results.length !== 1 ? 's' : ''}`);
|
||||
});
|
||||
|
||||
splitSearch?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') { splitPane.classList.add('hidden'); e.stopPropagation(); }
|
||||
if (e.key === 'Enter') {
|
||||
const first = splitPicks.querySelector('[data-split-card]');
|
||||
if (first) loadSplitCard(first.dataset.splitCard);
|
||||
}
|
||||
});
|
||||
|
||||
$('detail-edit')?.addEventListener('click', () => {
|
||||
if (!state.viewingCard) return;
|
||||
const card = state.viewingCard;
|
||||
|
||||
Reference in new issue
Block a user