feat(canvas): enhanced feed view with cards, sections, and scroll navigation
Replaces minimal feed mode with a polished scroll-through view: shapes wrapped in card containers with icon/title/type headers, grouped by section (type, date, position, alpha) with dividers, sticky scroll summary bar with item counter and clickable section chips for quick navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c19142791e
commit
eedf2cf189
|
|
@ -527,10 +527,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
/* Feed mode: hide bottom toolbar */
|
||||
#canvas.feed-mode ~ #bottom-toolbar {
|
||||
display: none;
|
||||
}
|
||||
/* Feed mode: hide bottom toolbar (overridden by comprehensive rule below) */
|
||||
|
||||
#community-info {
|
||||
display: none;
|
||||
|
|
@ -1397,34 +1394,252 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
touch-action: pan-y;
|
||||
scroll-behavior: smooth;
|
||||
background: var(--rs-bg-page);
|
||||
}
|
||||
#canvas.feed-mode #canvas-content {
|
||||
transform: none !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 8px 80px;
|
||||
gap: 0;
|
||||
padding: 100px 8px 80px;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
/* Hide canvas-only UI in feed mode */
|
||||
body:has(#canvas.feed-mode) #toolbar,
|
||||
body:has(#canvas.feed-mode) #bottom-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
/* Keep corner tools but hide zoom, show only feed toggle */
|
||||
body:has(#canvas.feed-mode) #canvas-corner-tools #zoom-in,
|
||||
body:has(#canvas.feed-mode) #canvas-corner-tools #zoom-out,
|
||||
body:has(#canvas.feed-mode) #canvas-corner-tools #reset-view,
|
||||
body:has(#canvas.feed-mode) #canvas-corner-tools .corner-sep {
|
||||
display: none !important;
|
||||
}
|
||||
/* Target ALL shape types in feed mode */
|
||||
#canvas.feed-mode folk-shape,
|
||||
#canvas.feed-mode [is="folk-shape"] {
|
||||
#canvas.feed-mode [is="folk-shape"],
|
||||
#canvas.feed-mode folk-markdown,
|
||||
#canvas.feed-mode folk-wrapper,
|
||||
#canvas.feed-mode folk-slide,
|
||||
#canvas.feed-mode folk-chat,
|
||||
#canvas.feed-mode folk-obs-note,
|
||||
#canvas.feed-mode folk-rapp,
|
||||
#canvas.feed-mode folk-embed,
|
||||
#canvas.feed-mode folk-drawfast,
|
||||
#canvas.feed-mode folk-prompt,
|
||||
#canvas.feed-mode folk-zine-gen,
|
||||
#canvas.feed-mode folk-workflow-block,
|
||||
#canvas.feed-mode folk-choice-vote,
|
||||
#canvas.feed-mode folk-choice-rank,
|
||||
#canvas.feed-mode folk-choice-spider,
|
||||
#canvas.feed-mode folk-spider-3d,
|
||||
#canvas.feed-mode folk-choice-conviction,
|
||||
#canvas.feed-mode folk-token,
|
||||
#canvas.feed-mode folk-token-mint,
|
||||
#canvas.feed-mode folk-token-ledger,
|
||||
#canvas.feed-mode folk-google-item,
|
||||
#canvas.feed-mode folk-social-post,
|
||||
#canvas.feed-mode folk-calendar,
|
||||
#canvas.feed-mode folk-map,
|
||||
#canvas.feed-mode folk-piano,
|
||||
#canvas.feed-mode folk-splat,
|
||||
#canvas.feed-mode folk-video-chat,
|
||||
#canvas.feed-mode folk-transcription,
|
||||
#canvas.feed-mode folk-image-gen,
|
||||
#canvas.feed-mode folk-video-gen,
|
||||
#canvas.feed-mode folk-blender,
|
||||
#canvas.feed-mode folk-freecad,
|
||||
#canvas.feed-mode folk-kicad,
|
||||
#canvas.feed-mode folk-itinerary,
|
||||
#canvas.feed-mode folk-destination,
|
||||
#canvas.feed-mode folk-budget,
|
||||
#canvas.feed-mode folk-packing-list,
|
||||
#canvas.feed-mode folk-booking,
|
||||
#canvas.feed-mode folk-feed {
|
||||
position: relative !important;
|
||||
transform: none !important;
|
||||
width: min(100%, 480px) !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 200px;
|
||||
min-height: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Hide arrows/connections in feed mode */
|
||||
/* Wrapper/canvas shapes get taller min-height */
|
||||
#canvas.feed-mode folk-wrapper,
|
||||
#canvas.feed-mode folk-slide {
|
||||
min-height: 300px;
|
||||
}
|
||||
/* Hide arrows in feed mode */
|
||||
#canvas.feed-mode folk-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
/* Hide grid background in feed mode */
|
||||
#canvas.feed-mode {
|
||||
background: var(--rs-bg-page);
|
||||
|
||||
/* ── Feed card wrappers ── */
|
||||
.feed-card {
|
||||
width: min(100%, 600px);
|
||||
margin: 8px auto;
|
||||
border: 1px solid var(--rs-border, #e2e8f0);
|
||||
border-radius: 12px;
|
||||
background: var(--rs-bg-card, #fff);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
body[data-theme="dark"] .feed-card {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
}
|
||||
.feed-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--rs-border, #e2e8f0);
|
||||
background: var(--rs-bg-subtle, #f8fafc);
|
||||
font-size: 13px;
|
||||
min-height: 36px;
|
||||
}
|
||||
body[data-theme="dark"] .feed-card-header {
|
||||
background: #0f172a;
|
||||
border-color: #334155;
|
||||
}
|
||||
.feed-card-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.feed-card-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
color: var(--rs-text, #1e293b);
|
||||
}
|
||||
body[data-theme="dark"] .feed-card-title {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.feed-card-type {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--rs-accent, #6366f1);
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.feed-card-body {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Feed section headers ── */
|
||||
.feed-section-header {
|
||||
width: min(100%, 600px);
|
||||
margin: 20px auto 4px;
|
||||
padding: 8px 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 2px solid var(--rs-accent, #6366f1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
body[data-theme="dark"] .feed-section-header {
|
||||
color: #94a3b8;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
.feed-section-header:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* ── Feed scroll summary bar ── */
|
||||
#feed-scroll-summary {
|
||||
position: fixed;
|
||||
top: 148px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 14px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
|
||||
z-index: 1002;
|
||||
font-size: 13px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
body[data-theme="dark"] #feed-scroll-summary {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
#feed-scroll-summary.visible {
|
||||
display: flex;
|
||||
}
|
||||
.feed-summary-section {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: var(--rs-text, #1e293b);
|
||||
}
|
||||
body[data-theme="dark"] .feed-summary-section {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.feed-summary-counter {
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.feed-summary-chips {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.feed-summary-chips::-webkit-scrollbar { display: none; }
|
||||
.feed-chip {
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--rs-bg-subtle, #f1f5f9);
|
||||
border: 1px solid var(--rs-border, #e2e8f0);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
body[data-theme="dark"] .feed-chip {
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.feed-chip:hover {
|
||||
background: var(--rs-accent, #6366f1);
|
||||
color: white;
|
||||
border-color: var(--rs-accent, #6366f1);
|
||||
}
|
||||
.feed-chip.active {
|
||||
background: var(--rs-accent, #6366f1);
|
||||
color: white;
|
||||
border-color: var(--rs-accent, #6366f1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#feed-scroll-summary {
|
||||
top: 100px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
transform: none;
|
||||
}
|
||||
#canvas.feed-mode #canvas-content {
|
||||
padding-top: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
#canvas-content.hide-forgotten :state(forgotten) {
|
||||
|
|
@ -1933,6 +2148,12 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div id="feed-scroll-summary">
|
||||
<span class="feed-summary-section"></span>
|
||||
<span class="feed-summary-counter"></span>
|
||||
<div class="feed-summary-chips"></div>
|
||||
</div>
|
||||
|
||||
<div id="toolbar">
|
||||
<button id="toolbar-collapse" title="Minimize toolbar"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="6" y1="18" x2="18" y2="18"/></svg></button>
|
||||
|
||||
|
|
@ -5702,43 +5923,41 @@
|
|||
});
|
||||
|
||||
// ── Feed Mode ──
|
||||
const FEED_SHAPE_SELECTOR = 'folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-obs-note, folk-rapp, folk-embed, folk-drawfast, folk-prompt, folk-zine-gen, folk-workflow-block, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-spider-3d, folk-choice-conviction, folk-token, folk-token-mint, folk-token-ledger, folk-google-item, folk-social-post, folk-calendar, folk-map, folk-piano, folk-splat, folk-video-chat, folk-transcription, folk-image-gen, folk-video-gen, folk-blender, folk-freecad, folk-kicad, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking, folk-feed';
|
||||
|
||||
let feedMode = false;
|
||||
let feedSortKey = 'y';
|
||||
let feedOriginalOrder = [];
|
||||
let feedScrollObserver = null;
|
||||
const feedToggleBtn = document.getElementById('feed-toggle');
|
||||
const feedSortBar = document.getElementById('feed-sort-bar');
|
||||
const feedSortSelect = document.getElementById('feed-sort');
|
||||
const feedScrollSummary = document.getElementById('feed-scroll-summary');
|
||||
const feedSummarySection = feedScrollSummary.querySelector('.feed-summary-section');
|
||||
const feedSummaryCounter = feedScrollSummary.querySelector('.feed-summary-counter');
|
||||
const feedSummaryChips = feedScrollSummary.querySelector('.feed-summary-chips');
|
||||
|
||||
function toggleFeedMode() {
|
||||
feedMode = !feedMode;
|
||||
canvas.classList.toggle('feed-mode', feedMode);
|
||||
feedToggleBtn.classList.toggle('active', feedMode);
|
||||
feedSortBar.classList.toggle('hidden', !feedMode);
|
||||
if (feedMode) {
|
||||
// Save original DOM order before reordering
|
||||
feedOriginalOrder = [...canvasContent.children];
|
||||
sortFeedShapes(feedSortKey);
|
||||
} else {
|
||||
// Restore original DOM order
|
||||
for (const el of feedOriginalOrder) canvasContent.appendChild(el);
|
||||
feedOriginalOrder = [];
|
||||
}
|
||||
// Human-readable type name from tag
|
||||
function feedTypeName(tag) {
|
||||
return (tag || '').replace('folk-', '').replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
function sortFeedShapes(key) {
|
||||
const shapes = [...canvasContent.querySelectorAll(
|
||||
'folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-obs-note, folk-rapp, folk-embed, folk-drawfast, folk-prompt, folk-zine-gen, folk-workflow-block, folk-choice-vote, folk-choice-rank, folk-choice-spider, folk-spider-3d, folk-choice-conviction, folk-token, folk-google-item, folk-social-post, folk-calendar, folk-map, folk-piano, folk-splat, folk-video-chat, folk-transcription, folk-image-gen, folk-video-gen, folk-zine-gen, folk-blender, folk-freecad, folk-kicad, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking'
|
||||
)];
|
||||
// Get label for a shape element
|
||||
function feedShapeLabel(el) {
|
||||
const d = sync.doc?.shapes?.[el.id] || {};
|
||||
return d.title || d.content?.slice(0, 40) || d.tokenName || d.label ||
|
||||
el.getAttribute('title') || el.textContent?.trim().slice(0, 40) || feedTypeName(el.tagName.toLowerCase());
|
||||
}
|
||||
|
||||
shapes.sort((a, b) => {
|
||||
// Read from Automerge doc when available
|
||||
// Sort shapes array by key, return sorted array
|
||||
function feedSortArray(shapes, key) {
|
||||
return shapes.sort((a, b) => {
|
||||
const da = (sync.doc?.shapes?.[a.id]) || {};
|
||||
const db = (sync.doc?.shapes?.[b.id]) || {};
|
||||
switch (key) {
|
||||
case 'y':
|
||||
return (parseFloat(da.y ?? a.y ?? 0)) - (parseFloat(db.y ?? b.y ?? 0));
|
||||
case 'created': {
|
||||
// Shape IDs encode timestamp: shape-<timestamp>-<counter>
|
||||
const ta = parseInt(a.id?.split('-')[1]) || 0;
|
||||
const tb = parseInt(b.id?.split('-')[1]) || 0;
|
||||
return ta - tb;
|
||||
|
|
@ -5750,12 +5969,229 @@
|
|||
const nameB = db.title || db.content || b.getAttribute('title') || b.textContent?.slice(0, 50) || '';
|
||||
return nameA.localeCompare(nameB);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const s of shapes) canvasContent.appendChild(s);
|
||||
// Compute section groupings for sorted shapes
|
||||
function feedComputeSections(shapes, key) {
|
||||
const sections = []; // [{name, items: [el, ...]}]
|
||||
for (const el of shapes) {
|
||||
const d = sync.doc?.shapes?.[el.id] || {};
|
||||
let sectionName;
|
||||
switch (key) {
|
||||
case 'type': {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const icon = SHAPE_ICONS[tag] || '';
|
||||
sectionName = icon + ' ' + feedTypeName(tag);
|
||||
break;
|
||||
}
|
||||
case 'created': {
|
||||
const ts = parseInt(el.id?.split('-')[1]) || 0;
|
||||
if (!ts) { sectionName = 'Unknown'; break; }
|
||||
const date = new Date(ts);
|
||||
const now = new Date();
|
||||
const dayDiff = Math.floor((now - date) / 86400000);
|
||||
if (dayDiff === 0) sectionName = 'Today';
|
||||
else if (dayDiff <= 7) sectionName = 'This Week';
|
||||
else if (dayDiff <= 30) sectionName = 'This Month';
|
||||
else sectionName = 'Older';
|
||||
break;
|
||||
}
|
||||
case 'alpha': {
|
||||
const label = (d.title || d.content || el.getAttribute('title') || el.textContent?.slice(0, 1) || '').trim();
|
||||
const first = (label[0] || '#').toUpperCase();
|
||||
sectionName = /[A-Z]/.test(first) ? first : '#';
|
||||
break;
|
||||
}
|
||||
case 'y': {
|
||||
const y = parseFloat(d.y ?? el.y ?? 0);
|
||||
// Compute range from all shapes
|
||||
if (!feedComputeSections._yRange) {
|
||||
const allY = shapes.map(s => parseFloat((sync.doc?.shapes?.[s.id] || {}).y ?? s.y ?? 0));
|
||||
const minY = Math.min(...allY);
|
||||
const maxY = Math.max(...allY);
|
||||
const range = maxY - minY || 1;
|
||||
feedComputeSections._yRange = { minY, range };
|
||||
}
|
||||
const { minY, range } = feedComputeSections._yRange;
|
||||
const pct = (y - minY) / range;
|
||||
if (pct < 0.33) sectionName = 'Top';
|
||||
else if (pct < 0.66) sectionName = 'Middle';
|
||||
else sectionName = 'Bottom';
|
||||
break;
|
||||
}
|
||||
default: sectionName = 'All';
|
||||
}
|
||||
const last = sections[sections.length - 1];
|
||||
if (last && last.name === sectionName) {
|
||||
last.items.push(el);
|
||||
} else {
|
||||
sections.push({ name: sectionName, items: [el] });
|
||||
}
|
||||
}
|
||||
feedComputeSections._yRange = null; // reset
|
||||
return sections;
|
||||
}
|
||||
|
||||
// Build feed DOM: section headers + feed cards
|
||||
function feedBuildDOM(sections) {
|
||||
// Remove any existing feed elements
|
||||
canvasContent.querySelectorAll('.feed-card, .feed-section-header').forEach(el => el.remove());
|
||||
|
||||
let itemIndex = 0;
|
||||
for (const section of sections) {
|
||||
// Section header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'feed-section-header';
|
||||
header.dataset.section = section.name;
|
||||
header.textContent = section.name + ' (' + section.items.length + ')';
|
||||
canvasContent.appendChild(header);
|
||||
|
||||
for (const shape of section.items) {
|
||||
itemIndex++;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'feed-card';
|
||||
card.dataset.feedIndex = itemIndex;
|
||||
card.dataset.section = section.name;
|
||||
|
||||
const cardHeader = document.createElement('div');
|
||||
cardHeader.className = 'feed-card-header';
|
||||
|
||||
const tag = shape.tagName.toLowerCase();
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'feed-card-icon';
|
||||
icon.textContent = SHAPE_ICONS[tag] || '📄';
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.className = 'feed-card-title';
|
||||
title.textContent = feedShapeLabel(shape);
|
||||
|
||||
const typeBadge = document.createElement('span');
|
||||
typeBadge.className = 'feed-card-type';
|
||||
typeBadge.textContent = feedTypeName(tag);
|
||||
|
||||
cardHeader.append(icon, title, typeBadge);
|
||||
|
||||
const cardBody = document.createElement('div');
|
||||
cardBody.className = 'feed-card-body';
|
||||
cardBody.appendChild(shape);
|
||||
|
||||
card.append(cardHeader, cardBody);
|
||||
canvasContent.appendChild(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate section chips in scroll summary
|
||||
function feedPopulateChips(sections) {
|
||||
feedSummaryChips.innerHTML = '';
|
||||
for (const section of sections) {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'feed-chip';
|
||||
chip.textContent = section.name;
|
||||
chip.dataset.section = section.name;
|
||||
chip.addEventListener('click', () => scrollToSection(section.name));
|
||||
feedSummaryChips.appendChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
// Track scroll position to update summary bar
|
||||
function feedSetupScrollTracking() {
|
||||
feedTeardownScrollTracking();
|
||||
const headers = canvasContent.querySelectorAll('.feed-section-header');
|
||||
const cards = canvasContent.querySelectorAll('.feed-card');
|
||||
const totalItems = cards.length;
|
||||
|
||||
if (!headers.length) return;
|
||||
|
||||
// Use scroll event on the canvas element (it's the scrollable container in feed mode)
|
||||
const scrollHandler = () => {
|
||||
const scrollTop = canvas.scrollTop;
|
||||
const viewportMid = scrollTop + 140; // account for fixed bars
|
||||
|
||||
// Find current section
|
||||
let currentSection = '';
|
||||
for (const h of headers) {
|
||||
if (h.offsetTop <= viewportMid) {
|
||||
currentSection = h.dataset.section;
|
||||
}
|
||||
}
|
||||
|
||||
// Count current item index
|
||||
let currentIndex = 0;
|
||||
for (const c of cards) {
|
||||
if (c.offsetTop <= viewportMid) currentIndex = parseInt(c.dataset.feedIndex);
|
||||
}
|
||||
|
||||
feedSummarySection.textContent = currentSection;
|
||||
feedSummaryCounter.textContent = currentIndex + ' of ' + totalItems;
|
||||
|
||||
// Update active chip
|
||||
feedSummaryChips.querySelectorAll('.feed-chip').forEach(ch => {
|
||||
ch.classList.toggle('active', ch.dataset.section === currentSection);
|
||||
});
|
||||
};
|
||||
|
||||
canvas.addEventListener('scroll', scrollHandler, { passive: true });
|
||||
canvas._feedScrollHandler = scrollHandler;
|
||||
// Initial update
|
||||
scrollHandler();
|
||||
}
|
||||
|
||||
function feedTeardownScrollTracking() {
|
||||
if (canvas._feedScrollHandler) {
|
||||
canvas.removeEventListener('scroll', canvas._feedScrollHandler);
|
||||
canvas._feedScrollHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToSection(sectionName) {
|
||||
const header = canvasContent.querySelector('.feed-section-header[data-section="' + CSS.escape(sectionName) + '"]');
|
||||
if (header) {
|
||||
canvas.scrollTo({ top: header.offsetTop - 130, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFeedMode() {
|
||||
feedMode = !feedMode;
|
||||
canvas.classList.toggle('feed-mode', feedMode);
|
||||
feedToggleBtn.classList.toggle('active', feedMode);
|
||||
feedSortBar.classList.toggle('hidden', !feedMode);
|
||||
feedScrollSummary.classList.toggle('visible', feedMode);
|
||||
if (feedMode) {
|
||||
feedOriginalOrder = [...canvasContent.children];
|
||||
sortFeedShapes(feedSortKey);
|
||||
} else {
|
||||
// Teardown scroll tracking
|
||||
feedTeardownScrollTracking();
|
||||
// Unwrap shapes from feed cards back into canvas-content
|
||||
canvasContent.querySelectorAll('.feed-card-body').forEach(body => {
|
||||
while (body.firstChild) canvasContent.appendChild(body.firstChild);
|
||||
});
|
||||
// Remove feed cards and section headers
|
||||
canvasContent.querySelectorAll('.feed-card, .feed-section-header').forEach(el => el.remove());
|
||||
// Restore original DOM order
|
||||
for (const el of feedOriginalOrder) canvasContent.appendChild(el);
|
||||
feedOriginalOrder = [];
|
||||
feedScrollSummary.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function sortFeedShapes(key) {
|
||||
// If currently wrapped in cards, unwrap first
|
||||
canvasContent.querySelectorAll('.feed-card-body').forEach(body => {
|
||||
while (body.firstChild) canvasContent.appendChild(body.firstChild);
|
||||
});
|
||||
canvasContent.querySelectorAll('.feed-card, .feed-section-header').forEach(el => el.remove());
|
||||
|
||||
const shapes = [...canvasContent.querySelectorAll(FEED_SHAPE_SELECTOR)];
|
||||
feedSortArray(shapes, key);
|
||||
const sections = feedComputeSections(shapes, key);
|
||||
feedBuildDOM(sections);
|
||||
feedPopulateChips(sections);
|
||||
feedSetupScrollTracking();
|
||||
}
|
||||
|
||||
feedToggleBtn.addEventListener('click', toggleFeedMode);
|
||||
|
|
|
|||
Loading…
Reference in New Issue