diff --git a/app/styles.css b/app/styles.css index 8f93890..ee8ef78 100644 --- a/app/styles.css +++ b/app/styles.css @@ -1260,6 +1260,8 @@ --frame-cell-height: calc(var(--frame-cell-width) * 1.5); --frame-base-gap: clamp(2px, 0.3vw, 6px); --frame-gap: calc(var(--frame-base-gap) * var(--frame-grid-zoom-scale)); + --frame-pan-cushion-inline: calc(clamp(14px, 1.8vw, 26px) * var(--frame-grid-zoom-scale)); + --frame-pan-cushion-block: calc(clamp(18px, 2.4vw, 34px) * var(--frame-grid-zoom-scale)); display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; @@ -1431,8 +1433,14 @@ .tarot-frame-grid-track { width: max-content; min-width: max-content; + padding: + var(--frame-pan-cushion-block) + var(--frame-pan-cushion-inline) + calc(var(--frame-pan-cushion-block) * 1.45) + var(--frame-pan-cushion-inline); margin-left: auto; margin-right: auto; + box-sizing: border-box; } .tarot-frame-legend { diff --git a/app/ui-tarot-frame.js b/app/ui-tarot-frame.js index 6e4b7fa..37321ce 100644 --- a/app/ui-tarot-frame.js +++ b/app/ui-tarot-frame.js @@ -59,6 +59,7 @@ const HOUSE_BOTTOM_INFO_MODE_IDS = ["zodiac", "decan", "month", "ruler", "date"]; const FRAME_LONG_PRESS_DELAY_MS = 460; const FRAME_LONG_PRESS_MOVE_TOLERANCE = 10; + const FRAME_TOUCH_DRAG_ACTIVATION_DELAY_MS = 140; const EXPORT_SLOT_WIDTH = 120; const EXPORT_SLOT_HEIGHT = Math.round((EXPORT_SLOT_WIDTH * TAROT_CARD_HEIGHT_RATIO) / TAROT_CARD_WIDTH_RATIO); const EXPORT_CARD_INSET = 0; @@ -836,6 +837,23 @@ return viewportEl instanceof HTMLElement ? viewportEl : null; } + function isLayoutNoteTextarea(element) { + return element instanceof HTMLTextAreaElement && element.id === "tarot-frame-layout-note"; + } + + function blurLayoutNoteForBoardInteraction(target) { + const activeElement = document.activeElement; + if (!isLayoutNoteTextarea(activeElement)) { + return; + } + + if (target instanceof Node && target === activeElement) { + return; + } + + activeElement.blur(); + } + function updateViewportInteractionState() { const viewportEl = getGridViewportElement(); if (!(viewportEl instanceof HTMLElement)) { @@ -1369,6 +1387,7 @@ return; } + removeOrphanedDragGhosts(); startPanGesture(null, { source: "touch", startX: anchor.x, @@ -1386,6 +1405,7 @@ return; } + removeOrphanedDragGhosts(); clearLongPressGesture(); if (state.drag) { cleanupDrag(); @@ -1457,6 +1477,8 @@ } function handleBoardTouchStart(event) { + blurLayoutNoteForBoardInteraction(event.target); + if (event.touches.length >= 3) { startTouchPanGesture(event); return; @@ -3123,6 +3145,15 @@ setHoverSlot(nextSlotId && nextSlotId !== sourceSlotId ? nextSlotId : ""); } + function removeOrphanedDragGhosts() { + document.querySelectorAll(".tarot-frame-drag-ghost").forEach((ghostEl) => { + if (ghostEl instanceof HTMLElement) { + ghostEl.remove(); + } + }); + document.body.classList.remove("is-tarot-frame-dragging"); + } + function detachPointerListeners() { document.removeEventListener("pointermove", handlePointerMove); document.removeEventListener("pointerup", handlePointerUp); @@ -3131,6 +3162,7 @@ function cleanupDrag() { if (!state.drag) { + removeOrphanedDragGhosts(); return; } @@ -3151,10 +3183,25 @@ } state.drag = null; - document.body.classList.remove("is-tarot-frame-dragging"); + removeOrphanedDragGhosts(); detachPointerListeners(); } + function handleDocumentTouchStart(event) { + if (Number(event.touches?.length || 0) < 2) { + return; + } + + clearLongPressGesture(); + if (state.drag?.pointerType === "touch") { + cleanupDrag(); + state.suppressClick = true; + return; + } + + removeOrphanedDragGhosts(); + } + function swapOrMoveSlots(sourceSlotId, targetSlotId) { const sourceCardId = String(state.slotAssignments.get(sourceSlotId) || ""); const targetCardId = String(state.slotAssignments.get(targetSlotId) || ""); @@ -3211,6 +3258,8 @@ return; } + blurLayoutNoteForBoardInteraction(target); + if (event.button === 1) { startPanGesture(event, { source: "pointer" }); return; @@ -3237,10 +3286,14 @@ state.drag = { pointerId: event.pointerId, + pointerType: String(event.pointerType || "").toLowerCase(), sourceSlotId: String(cardButton.dataset.slotId || ""), cardId: String(cardButton.dataset.cardId || ""), startX: event.clientX, startY: event.clientY, + touchEligibleAt: String(event.pointerType || "").toLowerCase() === "touch" + ? (Number(event.timeStamp) || window.performance.now()) + FRAME_TOUCH_DRAG_ACTIVATION_DELAY_MS + : 0, started: false, hoverSlotId: "", ghostEl: null, @@ -3271,6 +3324,18 @@ return; } + if (state.drag.pointerType === "touch" && (state.pinchGesture || (state.panGesture && state.panGesture.source === "touch"))) { + cleanupDrag(); + state.suppressClick = true; + return; + } + + if (!state.drag.started + && state.drag.pointerType === "touch" + && (Number(event.timeStamp) || window.performance.now()) < Number(state.drag.touchEligibleAt || 0)) { + return; + } + const movedEnough = Math.hypot(event.clientX - state.drag.startX, event.clientY - state.drag.startY) >= 6; if (!state.drag.started && movedEnough) { const card = getCardMap(getCards()).get(state.drag.cardId) || null; @@ -3813,6 +3878,11 @@ tarotFrameBoardEl.addEventListener("touchstart", handleBoardTouchStart, { passive: false }); } + document.addEventListener("touchstart", handleDocumentTouchStart, { + capture: true, + passive: true + }); + if (tarotFrameOverviewEl) { tarotFrameOverviewEl.addEventListener("input", (event) => { const target = event.target; diff --git a/index.html b/index.html index 3627272..3ef99fe 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ - +