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) {
|
handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
|
||||||
// Handle touch events for mobile drag support
|
// Handle touch events for mobile drag support
|
||||||
if (event instanceof TouchEvent) {
|
if (event instanceof TouchEvent) {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const target = event.composedPath()[0] as HTMLElement;
|
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 isDragHandle = target?.closest?.(".header, [data-drag]") !== null;
|
||||||
const isValidDragTarget = target === this || isDragHandle;
|
const isValidDragTarget = target === this || isDragHandle;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,6 @@
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-position: -1px -1px;
|
background-position: -1px -1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
touch-action: none; /* Prevent browser gestures, handle manually */
|
touch-action: none; /* Prevent browser gestures, handle manually */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +252,10 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Touch-friendly resize handles */
|
/* Touch-friendly resize handles */
|
||||||
|
|
@ -334,29 +336,101 @@
|
||||||
outline-offset: 4px !important;
|
outline-offset: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile toolbar: icon-only scrollable strip */
|
/* Mobile menu toggle (hidden on desktop) */
|
||||||
@media (max-width: 768px) {
|
#mobile-menu {
|
||||||
#toolbar {
|
|
||||||
max-width: calc(100vw - 32px);
|
|
||||||
overflow-x: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
touch-action: pan-x;
|
|
||||||
}
|
|
||||||
#toolbar::-webkit-scrollbar {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#toolbar button {
|
|
||||||
max-width: 36px;
|
#mobile-zoom {
|
||||||
min-width: 36px;
|
display: none;
|
||||||
padding: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
#community-info {
|
#community-info {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#memory-panel {
|
#memory-panel {
|
||||||
max-width: calc(100vw - 32px);
|
max-width: calc(100vw - 32px);
|
||||||
}
|
}
|
||||||
|
|
@ -369,6 +443,13 @@
|
||||||
<p id="community-slug"></p>
|
<p id="community-slug"></p>
|
||||||
</div>
|
</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">
|
<div id="toolbar">
|
||||||
<button id="new-markdown" title="New Note">📝 Note</button>
|
<button id="new-markdown" title="New Note">📝 Note</button>
|
||||||
<button id="new-wrapper" title="New Card">🗂️ Card</button>
|
<button id="new-wrapper" title="New Card">🗂️ Card</button>
|
||||||
|
|
@ -1263,6 +1344,44 @@
|
||||||
updateCanvasTransform();
|
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
|
// Touch gesture handling for pinch-to-zoom and two-finger pan
|
||||||
let initialDistance = 0;
|
let initialDistance = 0;
|
||||||
let initialScale = 1;
|
let initialScale = 1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue