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:
parent
34a1e3b640
commit
2d0cf499f6
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue