fix: mobile input focus, toolbar UX, and viewport clipping

- folk-shape: skip preventDefault on touch events targeting inputs/textareas
  so mobile keyboards can open inside shapes
- toolbar: replace unreadable emoji-only strip with FAB toggle (+) that
  opens a 3-column grid overlay with emoji + labels; auto-closes on tool
  select; separate always-visible zoom strip at bottom-left
- canvas: remove overflow:hidden from #canvas so viewport moves with
  pan/zoom instead of clipping at initial bounds
- canvas-content: add width/height 100% and overflow:visible for robust
  containing block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-20 19:55:43 +00:00
parent 34a1e3b640
commit 2d0cf499f6
2 changed files with 142 additions and 19 deletions

View File

@ -310,9 +310,13 @@ export class FolkShape extends FolkElement {
handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
// Handle touch events for mobile drag support
if (event instanceof TouchEvent) {
event.preventDefault();
const target = event.composedPath()[0] as HTMLElement;
// Allow interactive elements to receive focus on mobile
const tag = target?.tagName?.toLowerCase();
const isInteractive = tag === "input" || tag === "textarea" || tag === "select" || tag === "button" || target?.isContentEditable;
if (!isInteractive) {
event.preventDefault();
}
const isDragHandle = target?.closest?.(".header, [data-drag]") !== null;
const isValidDragTarget = target === this || isDragHandle;

View File

@ -245,7 +245,6 @@
background-size: 20px 20px;
background-position: -1px -1px;
position: relative;
overflow: hidden;
touch-action: none; /* Prevent browser gestures, handle manually */
}
@ -253,7 +252,10 @@
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform-origin: 0 0;
overflow: visible;
}
/* Touch-friendly resize handles */
@ -334,29 +336,101 @@
outline-offset: 4px !important;
}
/* Mobile toolbar: icon-only scrollable strip */
/* Mobile menu toggle (hidden on desktop) */
#mobile-menu {
display: none;
}
#mobile-zoom {
display: none;
}
@media (max-width: 768px) {
#toolbar {
max-width: calc(100vw - 32px);
overflow-x: auto;
scrollbar-width: none;
gap: 4px;
padding: 6px 8px;
touch-action: pan-x;
/* FAB toggle button */
#mobile-menu {
display: flex;
position: fixed;
bottom: 24px;
right: 16px;
width: 56px;
height: 56px;
border: none;
border-radius: 50%;
background: #14b8a6;
color: white;
font-size: 28px;
align-items: center;
justify-content: center;
z-index: 1002;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
cursor: pointer;
touch-action: manipulation;
}
#toolbar::-webkit-scrollbar {
/* Always-visible zoom strip */
#mobile-zoom {
display: flex;
position: fixed;
bottom: 24px;
left: 16px;
gap: 4px;
z-index: 1002;
}
#mobile-zoom button {
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
font-size: 16px;
cursor: pointer;
touch-action: manipulation;
}
/* Hide desktop toolbar, show as grid overlay when toggled */
#toolbar {
display: none;
position: fixed;
top: 72px;
left: 8px;
right: 8px;
transform: none;
flex-wrap: wrap;
max-height: calc(100vh - 160px);
overflow-y: auto;
gap: 6px;
padding: 12px;
border-radius: 16px;
z-index: 1001;
}
#toolbar.mobile-open {
display: flex;
}
#toolbar button {
flex: 0 0 calc(33.33% - 4px);
padding: 10px 4px;
font-size: 12px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hide zoom/reset from toolbar grid (they're in #mobile-zoom) */
#toolbar #zoom-in,
#toolbar #zoom-out,
#toolbar #reset-view {
display: none;
}
#toolbar button {
max-width: 36px;
min-width: 36px;
padding: 8px;
overflow: hidden;
white-space: nowrap;
}
#community-info {
display: none;
}
#memory-panel {
max-width: calc(100vw - 32px);
}
@ -369,6 +443,13 @@
<p id="community-slug"></p>
</div>
<button id="mobile-menu" title="Tools"></button>
<div id="mobile-zoom">
<button id="mz-out" title="Zoom Out"></button>
<button id="mz-in" title="Zoom In">+</button>
<button id="mz-reset" title="Reset View"></button>
</div>
<div id="toolbar">
<button id="new-markdown" title="New Note">📝 Note</button>
<button id="new-wrapper" title="New Card">🗂️ Card</button>
@ -1263,6 +1344,44 @@
updateCanvasTransform();
});
// Mobile toolbar toggle
const mobileMenuBtn = document.getElementById("mobile-menu");
const toolbarEl = document.getElementById("toolbar");
mobileMenuBtn.addEventListener("click", () => {
const isOpen = toolbarEl.classList.toggle("mobile-open");
mobileMenuBtn.textContent = isOpen ? "✕" : "✚";
});
// Auto-close toolbar after tapping a shape-creation button on mobile
toolbarEl.addEventListener("click", (e) => {
if (window.innerWidth > 768) return;
const btn = e.target.closest("button");
if (!btn) return;
// Keep open for connect, memory, zoom controls
const keepOpen = ["new-arrow", "toggle-memory", "zoom-in", "zoom-out", "reset-view"];
if (!keepOpen.includes(btn.id)) {
toolbarEl.classList.remove("mobile-open");
mobileMenuBtn.textContent = "✚";
}
});
// Mobile zoom controls (separate from toolbar)
document.getElementById("mz-in").addEventListener("click", () => {
scale = Math.min(scale * 1.25, maxScale);
updateCanvasTransform();
});
document.getElementById("mz-out").addEventListener("click", () => {
scale = Math.max(scale / 1.25, minScale);
updateCanvasTransform();
});
document.getElementById("mz-reset").addEventListener("click", () => {
scale = 1;
panX = 0;
panY = 0;
updateCanvasTransform();
});
// Touch gesture handling for pinch-to-zoom and two-finger pan
let initialDistance = 0;
let initialScale = 1;