Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
const MOBILE_SHEET_MQ = "(max-width: 768px)";
|
||||
|
||||
function getSheetParts(root) {
|
||||
if (!root) return null;
|
||||
const panel = root.querySelector(
|
||||
".recipe-mobile-edit-sheet-panel, .ingredient-mobile-sheet-panel, .recipe-help-modal-panel"
|
||||
);
|
||||
const body = root.querySelector(
|
||||
".recipe-mobile-edit-sheet-body, .ingredient-mobile-sheet-body, .recipe-help-modal-body"
|
||||
);
|
||||
const backdrop = root.querySelector(".recipe-mobile-edit-sheet-backdrop");
|
||||
return panel ? { root, panel, body, backdrop } : null;
|
||||
}
|
||||
|
||||
export function resetRecipeMobileSheetPresentation(root) {
|
||||
const parts = getSheetParts(root);
|
||||
if (!parts) return;
|
||||
const { panel, backdrop } = parts;
|
||||
panel.style.transform = "";
|
||||
panel.style.transition = "";
|
||||
panel.classList.remove("recipe-mobile-edit-sheet--dragging");
|
||||
if (backdrop) {
|
||||
backdrop.style.opacity = "";
|
||||
backdrop.style.transition = "";
|
||||
}
|
||||
}
|
||||
|
||||
function canStartSheetDrag(target, body) {
|
||||
if (target.closest(".recipe-mobile-edit-sheet-done")) return false;
|
||||
if (target.closest("button, input, select, textarea, a, label")) {
|
||||
return !!target.closest(".recipe-mobile-edit-sheet-grab, .recipe-mobile-edit-sheet-title");
|
||||
}
|
||||
if (target.closest(".recipe-mobile-edit-sheet-grab, .recipe-mobile-edit-sheet-header")) {
|
||||
return true;
|
||||
}
|
||||
if (body?.contains(target) && body.scrollTop <= 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function bindSheetGestures(root, onClose, mobileQuery) {
|
||||
const parts = getSheetParts(root);
|
||||
if (!parts || typeof onClose !== "function") return;
|
||||
|
||||
const { panel, body, backdrop } = parts;
|
||||
let dragState = null;
|
||||
|
||||
function clearDragState() {
|
||||
dragState = null;
|
||||
panel.classList.remove("recipe-mobile-edit-sheet--dragging");
|
||||
}
|
||||
|
||||
function applyDrag(deltaY) {
|
||||
const offset = Math.max(0, deltaY);
|
||||
panel.style.transition = "none";
|
||||
panel.style.transform = `translateY(${offset}px)`;
|
||||
if (backdrop) {
|
||||
backdrop.style.transition = "none";
|
||||
backdrop.style.opacity = String(Math.max(0, 1 - offset / 280));
|
||||
}
|
||||
}
|
||||
|
||||
function snapBack() {
|
||||
panel.style.transition = "transform 0.24s ease";
|
||||
panel.style.transform = "";
|
||||
if (backdrop) {
|
||||
backdrop.style.transition = "opacity 0.24s ease";
|
||||
backdrop.style.opacity = "";
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
panel.style.transition = "";
|
||||
if (backdrop) backdrop.style.transition = "";
|
||||
clearDragState();
|
||||
}, 240);
|
||||
}
|
||||
|
||||
function dismissWithAnimation() {
|
||||
panel.style.transition = "transform 0.22s ease";
|
||||
panel.style.transform = "translateY(100%)";
|
||||
if (backdrop) {
|
||||
backdrop.style.transition = "opacity 0.22s ease";
|
||||
backdrop.style.opacity = "0";
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
resetRecipeMobileSheetPresentation(root);
|
||||
onClose();
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function onPointerDown(event) {
|
||||
if (!mobileQuery.matches || root.hidden) return;
|
||||
if (event.pointerType === "mouse" && event.button !== 0) return;
|
||||
if (!canStartSheetDrag(event.target, body)) return;
|
||||
|
||||
dragState = {
|
||||
pointerId: event.pointerId,
|
||||
startY: event.clientY,
|
||||
fromScrollableBody: !!(body && body.contains(event.target) && !event.target.closest(".recipe-mobile-edit-sheet-header, .recipe-mobile-edit-sheet-grab")),
|
||||
};
|
||||
panel.classList.add("recipe-mobile-edit-sheet--dragging");
|
||||
panel.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function onPointerMove(event) {
|
||||
if (!dragState || event.pointerId !== dragState.pointerId) return;
|
||||
|
||||
const deltaY = event.clientY - dragState.startY;
|
||||
if (dragState.fromScrollableBody) {
|
||||
if (deltaY <= 0 || (body && body.scrollTop > 0)) {
|
||||
clearDragState();
|
||||
resetRecipeMobileSheetPresentation(root);
|
||||
return;
|
||||
}
|
||||
if (deltaY > 8) {
|
||||
dragState.fromScrollableBody = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (deltaY > 0) {
|
||||
event.preventDefault();
|
||||
applyDrag(deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerEnd(event) {
|
||||
if (!dragState || event.pointerId !== dragState.pointerId) return;
|
||||
|
||||
const deltaY = event.clientY - dragState.startY;
|
||||
const threshold = Math.min(96, panel.offsetHeight * 0.2);
|
||||
clearDragState();
|
||||
|
||||
if (deltaY > threshold) {
|
||||
dismissWithAnimation();
|
||||
return;
|
||||
}
|
||||
snapBack();
|
||||
}
|
||||
|
||||
panel.addEventListener("pointerdown", onPointerDown);
|
||||
panel.addEventListener("pointermove", onPointerMove, { passive: false });
|
||||
panel.addEventListener("pointerup", onPointerEnd);
|
||||
panel.addEventListener("pointercancel", onPointerEnd);
|
||||
}
|
||||
|
||||
export function initRecipeMobileSheetGestures(handlers = {}) {
|
||||
const mobileQuery = window.matchMedia(MOBILE_SHEET_MQ);
|
||||
const sheets = [
|
||||
{
|
||||
root: document.getElementById("ingredientMobileSheet"),
|
||||
onClose: handlers.closeIngredientMobileSheet,
|
||||
},
|
||||
{
|
||||
root: document.getElementById("unloadingGroupMobileSheet"),
|
||||
onClose: handlers.closeUnloadingGroupMobileSheet,
|
||||
},
|
||||
{
|
||||
root: document.getElementById("recipeHelpModal"),
|
||||
onClose: handlers.closeRecipeHelpModal,
|
||||
},
|
||||
];
|
||||
|
||||
sheets.forEach(({ root, onClose }) => bindSheetGestures(root, onClose, mobileQuery));
|
||||
}
|
||||
Reference in New Issue
Block a user