Merge branch 'dev'
This commit is contained in:
commit
67820e7db5
|
|
@ -426,6 +426,9 @@ export class FolkShape extends FolkElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
|
handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
|
||||||
|
// In feed mode, suppress all drag/resize interactions
|
||||||
|
if (this.closest('#canvas.feed-mode')) return;
|
||||||
|
|
||||||
// Handle touch events for mobile drag support
|
// Handle touch events for mobile drag support
|
||||||
if (event instanceof TouchEvent) {
|
if (event instanceof TouchEvent) {
|
||||||
const target = event.composedPath()[0] as HTMLElement;
|
const target = event.composedPath()[0] as HTMLElement;
|
||||||
|
|
|
||||||
|
|
@ -1272,6 +1272,44 @@
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Feed mode ── */
|
||||||
|
#canvas.feed-mode {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
#canvas.feed-mode #canvas-content {
|
||||||
|
transform: none !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 8px 80px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#canvas.feed-mode folk-shape,
|
||||||
|
#canvas.feed-mode [is="folk-shape"] {
|
||||||
|
position: relative !important;
|
||||||
|
transform: none !important;
|
||||||
|
width: min(100%, 480px) !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 200px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
/* Hide arrows/connections in feed mode */
|
||||||
|
#canvas.feed-mode folk-arrow {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/* Hide grid background in feed mode */
|
||||||
|
#canvas.feed-mode {
|
||||||
|
background: var(--canvas-feed-bg, #f8fafc);
|
||||||
|
}
|
||||||
|
body[data-theme="dark"] #canvas.feed-mode {
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
#canvas-content.hide-forgotten :state(forgotten) {
|
#canvas-content.hide-forgotten :state(forgotten) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
@ -1507,6 +1545,78 @@
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Feed sort bar ── */
|
||||||
|
#feed-sort-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 108px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 1001;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
body[data-theme="dark"] #feed-sort-bar {
|
||||||
|
background: #1e293b;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
#feed-sort-bar.hidden { display: none; }
|
||||||
|
#feed-sort {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
body[data-theme="dark"] #feed-sort {
|
||||||
|
background: #334155;
|
||||||
|
border-color: #475569;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Feed toggle button ── */
|
||||||
|
#feed-toggle {
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
#feed-toggle:hover { background: #e2e8f0; }
|
||||||
|
#feed-toggle.active {
|
||||||
|
background: #14b8a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
body[data-theme="dark"] #feed-toggle {
|
||||||
|
background: #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
body[data-theme="dark"] #feed-toggle:hover { background: #475569; }
|
||||||
|
body[data-theme="dark"] #feed-toggle.active {
|
||||||
|
background: #14b8a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#feed-sort-bar {
|
||||||
|
top: 64px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/shell.css">
|
<link rel="stylesheet" href="/shell.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -1637,6 +1747,16 @@
|
||||||
<button id="mz-reset" title="Reset View">⟳</button>
|
<button id="mz-reset" title="Reset View">⟳</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="feed-sort-bar" class="hidden">
|
||||||
|
<span>Sort:</span>
|
||||||
|
<select id="feed-sort">
|
||||||
|
<option value="y">By Position</option>
|
||||||
|
<option value="created">By Created</option>
|
||||||
|
<option value="type">By Type</option>
|
||||||
|
<option value="alpha">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button class="toolbar-group-toggle">✏️ Draw</button>
|
<button class="toolbar-group-toggle">✏️ Draw</button>
|
||||||
|
|
@ -1745,6 +1865,7 @@
|
||||||
<button id="new-feed" title="New Feed from another layer">🔄 Feed</button>
|
<button id="new-feed" title="New Feed from another layer">🔄 Feed</button>
|
||||||
<button id="toggle-memory" title="Forgotten rSpaces">💭 Memory</button>
|
<button id="toggle-memory" title="Forgotten rSpaces">💭 Memory</button>
|
||||||
<button id="toggle-hide-forgotten" title="Hide forgotten items">👁 Hide Faded</button>
|
<button id="toggle-hide-forgotten" title="Hide forgotten items">👁 Hide Faded</button>
|
||||||
|
<button id="feed-toggle" title="Toggle feed view">📋 Feed View</button>
|
||||||
|
|
||||||
<span class="toolbar-sep"></span>
|
<span class="toolbar-sep"></span>
|
||||||
|
|
||||||
|
|
@ -4200,6 +4321,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener("touchstart", (e) => {
|
canvas.addEventListener("touchstart", (e) => {
|
||||||
|
if (feedMode) return;
|
||||||
if (e.touches.length === 2) {
|
if (e.touches.length === 2) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
isTouchPanning = true;
|
isTouchPanning = true;
|
||||||
|
|
@ -4220,6 +4342,7 @@
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
canvas.addEventListener("touchmove", (e) => {
|
canvas.addEventListener("touchmove", (e) => {
|
||||||
|
if (feedMode) return;
|
||||||
if (e.touches.length === 2 && isTouchPanning) {
|
if (e.touches.length === 2 && isTouchPanning) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -4253,6 +4376,7 @@
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
canvas.addEventListener("touchend", (e) => {
|
canvas.addEventListener("touchend", (e) => {
|
||||||
|
if (feedMode) return;
|
||||||
if (e.touches.length < 2) {
|
if (e.touches.length < 2) {
|
||||||
lastTouchCenter = null;
|
lastTouchCenter = null;
|
||||||
lastTouchDist = null;
|
lastTouchDist = null;
|
||||||
|
|
@ -4262,6 +4386,7 @@
|
||||||
|
|
||||||
// Mouse wheel / trackpad: pan (two-finger scroll = pan, Ctrl+wheel = zoom)
|
// Mouse wheel / trackpad: pan (two-finger scroll = pan, Ctrl+wheel = zoom)
|
||||||
canvas.addEventListener("wheel", (e) => {
|
canvas.addEventListener("wheel", (e) => {
|
||||||
|
if (feedMode) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
// Ctrl+wheel (or trackpad pinch) = zoom centered on cursor
|
// Ctrl+wheel (or trackpad pinch) = zoom centered on cursor
|
||||||
|
|
@ -4360,6 +4485,9 @@
|
||||||
// Whiteboard tool active → don't select or pan
|
// Whiteboard tool active → don't select or pan
|
||||||
if (wbTool) return;
|
if (wbTool) return;
|
||||||
|
|
||||||
|
// Feed mode: no canvas pan or select
|
||||||
|
if (feedMode) return;
|
||||||
|
|
||||||
// Middle-click or Space held → immediate PAN (no hold delay)
|
// Middle-click or Space held → immediate PAN (no hold delay)
|
||||||
if (e.button === 1 || spaceHeld) {
|
if (e.button === 1 || spaceHeld) {
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
|
|
@ -4567,6 +4695,62 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Feed Mode ──
|
||||||
|
let feedMode = false;
|
||||||
|
let feedSortKey = 'y';
|
||||||
|
const feedToggleBtn = document.getElementById('feed-toggle');
|
||||||
|
const feedSortBar = document.getElementById('feed-sort-bar');
|
||||||
|
const feedSortSelect = document.getElementById('feed-sort');
|
||||||
|
|
||||||
|
function toggleFeedMode() {
|
||||||
|
feedMode = !feedMode;
|
||||||
|
canvas.classList.toggle('feed-mode', feedMode);
|
||||||
|
feedToggleBtn.classList.toggle('active', feedMode);
|
||||||
|
feedSortBar.classList.toggle('hidden', !feedMode);
|
||||||
|
if (feedMode) {
|
||||||
|
sortFeedShapes(feedSortKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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-workflow-block, folk-choice-vote, folk-choice-rank, folk-choice-spider, 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-blender, folk-freecad, folk-kicad, folk-itinerary, folk-destination, folk-budget, folk-packing-list, folk-booking'
|
||||||
|
)];
|
||||||
|
|
||||||
|
shapes.sort((a, b) => {
|
||||||
|
// Read from Automerge doc when available
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
case 'type':
|
||||||
|
return (a.tagName || '').localeCompare(b.tagName || '');
|
||||||
|
case 'alpha': {
|
||||||
|
const nameA = da.title || da.content || a.getAttribute('title') || a.textContent?.slice(0, 50) || '';
|
||||||
|
const nameB = db.title || db.content || b.getAttribute('title') || b.textContent?.slice(0, 50) || '';
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const s of shapes) canvasContent.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
feedToggleBtn.addEventListener('click', toggleFeedMode);
|
||||||
|
feedSortSelect.addEventListener('change', (e) => {
|
||||||
|
feedSortKey = e.target.value;
|
||||||
|
if (feedMode) sortFeedShapes(feedSortKey);
|
||||||
|
});
|
||||||
|
|
||||||
sync.connect(wsUrl);
|
sync.connect(wsUrl);
|
||||||
|
|
||||||
// Debug: expose sync for console inspection
|
// Debug: expose sync for console inspection
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue