|
| 1 | +// Pod-local collection pane for the /public/bookmark/ container. |
| 2 | +// Renders every bookmark:Bookmark member as a searchable feed of compact cards. |
| 3 | +// Loaded by the `--browser panes` data browser from /public/panes/. |
| 4 | +// |
| 5 | +// canHandle matches the conventional bookmark container path; render fetches the |
| 6 | +// members and returns null if none are bookmarks (so the browser falls back to |
| 7 | +// the folder table). Search is a self-contained inline oninput handler — no |
| 8 | +// script tag (which wouldn't execute via innerHTML anyway). |
| 9 | + |
| 10 | +export default { |
| 11 | + canHandle(node, h) { |
| 12 | + return h.isContainer && /\/bookmark\/?$/.test(h.path || ''); |
| 13 | + }, |
| 14 | + |
| 15 | + async render(node, h) { |
| 16 | + const kids = (h.children || []).filter(it => it.type !== 'container' && it.url.endsWith('.jsonld')); |
| 17 | + const fetched = await Promise.all(kids.map(k => |
| 18 | + h.fetchResource(k.url).then(n => ({ url: k.url, n })).catch(() => null))); |
| 19 | + |
| 20 | + const items = []; |
| 21 | + for (const x of fetched) { |
| 22 | + if (!x || !x.n || !h.types(x.n).includes('Bookmark')) continue; |
| 23 | + const recalls = h.idOf(h.prop(x.n, 'recalls')); |
| 24 | + const title = h.first(h.prop(x.n, 'title')) || recalls || 'Untitled bookmark'; |
| 25 | + const lead = String(h.first(h.prop(x.n, 'description')) || '').split(/\n\s*\n/)[0].trim(); |
| 26 | + const snippet = lead.length > 190 ? lead.slice(0, 187) + '…' : lead; |
| 27 | + const tags = h.propAll(x.n, 'keywords').map(t => h.first(t)).filter(Boolean); |
| 28 | + const host = h.host(recalls); |
| 29 | + const created = h.first(h.prop(x.n, 'created')) || ''; |
| 30 | + const q = [title, lead, tags.join(' '), host].join(' ').toLowerCase(); |
| 31 | + items.push({ recalls, podUrl: x.url, title, snippet, tags, host, created, q }); |
| 32 | + } |
| 33 | + if (!items.length) return null; // not a bookmark collection → fall back to folder |
| 34 | + items.sort((a, b) => (b.created || '').localeCompare(a.created || '')); |
| 35 | + |
| 36 | + const card = it => { |
| 37 | + const fav = it.recalls ? h.escape(new URL('/favicon.ico', it.recalls).href) : ''; |
| 38 | + const chips = it.tags.slice(0, 5).map(t => `<span class="bmc-tag">${h.escape(t)}</span>`).join(''); |
| 39 | + // Card body → the bookmark's .jsonld detail page (full summary card). |
| 40 | + // Corner ↗ → the original external link. |
| 41 | + return `<div class="bmc-item" data-q="${h.escape(it.q)}"> |
| 42 | + <a class="bmc-main" href="${h.escape(it.podUrl)}"> |
| 43 | + <img class="bmc-fav" src="${fav}" alt="" onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🔖',style:'font-size:22px;line-height:34px;width:34px;text-align:center'}))"> |
| 44 | + <div class="bmc-body"> |
| 45 | + <div class="bmc-title">${h.escape(it.title)}</div> |
| 46 | + <div class="bmc-host">${h.escape(it.host)}</div> |
| 47 | + ${it.snippet ? `<div class="bmc-snippet">${h.escape(it.snippet)}</div>` : ''} |
| 48 | + ${chips ? `<div class="bmc-tags">${chips}</div>` : ''} |
| 49 | + </div> |
| 50 | + </a> |
| 51 | + <a class="bmc-ext" href="${h.escape(it.recalls)}" target="_blank" rel="noopener" title="Open original ↗" aria-label="Open original">↗</a> |
| 52 | + </div>`; |
| 53 | + }; |
| 54 | + |
| 55 | + const filter = "var r=this.closest('.bmc'),q=this.value.toLowerCase().trim(),n=0;" + |
| 56 | + "r.querySelectorAll('.bmc-item').forEach(function(el){var m=el.getAttribute('data-q').indexOf(q)>-1;el.style.display=m?'':'none';if(m)n++;});" + |
| 57 | + "r.querySelector('.bmc-count').textContent=n;" + |
| 58 | + "r.querySelector('.bmc-empty').style.display=n?'none':'block';"; |
| 59 | + |
| 60 | + return `<style> |
| 61 | + .bmc{max-width:720px;margin:0 auto;padding:8px 0 28px;font-family:Inter,-apple-system,system-ui,sans-serif;} |
| 62 | + .bmc-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:20px;} |
| 63 | + .bmc-eyebrow{font-family:Georgia,serif;font-style:italic;font-size:13px;color:#a1a1aa;} |
| 64 | + .bmc-title-main{font-size:24px;font-weight:650;color:#18181b;margin-top:5px;} |
| 65 | + .bmc-count{font-size:15px;color:#a1a1aa;font-weight:500;margin-left:5px;} |
| 66 | + .bmc-search{border:1px solid rgba(24,24,27,.12);border-radius:10px;padding:9px 13px;font-size:14px;outline:none;width:210px;max-width:45vw;background:#fff;font-family:inherit;transition:border-color .15s,box-shadow .15s;} |
| 67 | + .bmc-search:focus{border-color:#7c3aed;box-shadow:0 0 0 3px rgba(124,58,237,.12);} |
| 68 | + .bmc-list{display:flex;flex-direction:column;gap:12px;} |
| 69 | + .bmc-item{position:relative;background:#fff;border:1px solid rgba(24,24,27,.07);border-radius:14px;box-shadow:0 1px 2px rgba(24,24,27,.04);transition:box-shadow .15s,border-color .15s;} |
| 70 | + .bmc-item:hover{box-shadow:0 4px 18px rgba(24,24,27,.09);border-color:rgba(124,58,237,.25);} |
| 71 | + .bmc-main{display:flex;gap:14px;align-items:flex-start;text-decoration:none;color:inherit;padding:18px 52px 18px 20px;} |
| 72 | + .bmc-main:active{transform:translateY(1px);} |
| 73 | + .bmc-ext{position:absolute;top:13px;right:13px;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:8px;color:#b4b4bb;text-decoration:none;font-size:16px;line-height:1;transition:background .15s,color .15s;} |
| 74 | + .bmc-ext:hover{background:rgba(124,58,237,.1);color:#7c3aed;} |
| 75 | + .bmc-fav{width:34px;height:34px;border-radius:8px;flex:0 0 auto;background:rgba(127,127,127,.06);object-fit:contain;margin-top:2px;} |
| 76 | + .bmc-body{min-width:0;flex:1;} |
| 77 | + .bmc-title{font-size:16px;font-weight:600;color:#18181b;line-height:1.35;overflow-wrap:anywhere;} |
| 78 | + .bmc-host{font-size:12px;color:#7c3aed;font-family:ui-monospace,SFMono-Regular,monospace;margin-top:4px;} |
| 79 | + .bmc-snippet{font-size:13.5px;line-height:1.6;color:#52525b;margin-top:9px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;} |
| 80 | + .bmc-tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:11px;} |
| 81 | + .bmc-tag{font-size:11px;color:#6d28d9;background:rgba(124,58,237,.07);border:1px solid rgba(124,58,237,.12);padding:2px 9px;border-radius:999px;} |
| 82 | + .bmc-empty{text-align:center;color:#a1a1aa;padding:28px;font-size:14px;} |
| 83 | + </style> |
| 84 | + <div class="bmc"> |
| 85 | + <div class="bmc-head"> |
| 86 | + <div> |
| 87 | + <div class="bmc-eyebrow">Collection</div> |
| 88 | + <div class="bmc-title-main">Bookmarks <span class="bmc-count">${items.length}</span></div> |
| 89 | + </div> |
| 90 | + <input class="bmc-search" type="search" placeholder="Search title, summary, tags…" oninput="${filter}"> |
| 91 | + </div> |
| 92 | + <div class="bmc-list">${items.map(card).join('')}</div> |
| 93 | + <div class="bmc-empty" style="display:none">No matches.</div> |
| 94 | + </div>`; |
| 95 | + } |
| 96 | +}; |
0 commit comments