diff --git a/static/meal-planner/direct-planner.css b/static/meal-planner/direct-planner.css index 1964dc2..9b97a68 100644 --- a/static/meal-planner/direct-planner.css +++ b/static/meal-planner/direct-planner.css @@ -85,7 +85,7 @@ .planner-slot .planner-recipe-placed { position: relative; display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: 1fr; align-items: stretch; width: 100%; min-width: 0; @@ -117,36 +117,7 @@ transition: opacity .15s ease; } -.planner-recipe-placed .planner-recipe-thumb { grid-column: 1 / -1; } -.planner-recipe-placed .planner-recipe-actions { - position: absolute; - top: 6px; - right: 6px; - display: flex; - gap: 3px; - opacity: 0; - transition: opacity .15s ease; -} -.planner-recipe-placed:hover .planner-recipe-actions, -.planner-recipe-placed:focus-within .planner-recipe-actions { opacity: 1; } -.planner-order, -.planner-unplan { - width: 30px; - min-width: 30px; - height: 30px; - min-height: 30px; - padding: 0; - border: 1px solid rgb(255 255 255 / 70%); - border-radius: 7px; - background: rgb(0 0 0 / 60%); - color: #fff; - cursor: pointer; - font: inherit; - line-height: 1; -} -.planner-order:disabled { opacity: .35; cursor: default; } -.planner-unplan { color: #ffd2cc; font-size: 1.2rem; } - +.planner-recipe-placed .planner-recipe-thumb { grid-column: 1; } .planner-recipe-overlay strong { overflow: hidden; text-overflow: ellipsis; @@ -173,8 +144,6 @@ .planner-recipe-placed .planner-recipe-thumb { height: 88px; } - .planner-recipe-placed .planner-recipe-actions { opacity: 1; } - .planner-board-shell.planner-board-shell-full { display: block; } diff --git a/static/meal-planner/planner-app.js b/static/meal-planner/planner-app.js index dc46102..a9562dd 100644 --- a/static/meal-planner/planner-app.js +++ b/static/meal-planner/planner-app.js @@ -17,10 +17,12 @@ let planning = loadPlanning(); let weekStart = readWeek(); let catalog = new Map(); let selectedSlug = null; -let draggingSlug = null; let ignoreClickSlug = null; let feedbackTimer = null; +const nativeDragEnabled = () => !window.matchMedia("(pointer: coarse)").matches; +const draggableAttribute = () => nativeDragEnabled() ? ' draggable="true"' : ''; + const dates = () => Array.from({ length: 7 }, (_, index) => { const date = new Date(weekStart); date.setDate(weekStart.getDate() + index); return iso(date); }); const dayLabel = index => { const date = new Date(`${dates()[index]}T12:00:00`); return { day: DAYS[index], date: date.toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) }; }; const persist = () => { savePlanning(planning); localStorage.setItem(WEEK_KEY, iso(weekStart)); }; @@ -54,23 +56,6 @@ const removeFromSelection = slug => { document.dispatchEvent(new CustomEvent("cookigram:selection-change")); render(); }; -const reorder = (slug, direction) => { - const placement = planning[slug]; - if (!placement?.date || !placement?.moment) return; - const items = slotItems(placement.date, placement.moment); - const index = items.findIndex(item => item.slug === slug); - const target = index + direction; - if (index < 0 || target < 0 || target >= items.length) return; - const next = { ...planning }; - const first = items[index].slug; - const second = items[target].slug; - const firstOrder = next[first].order ?? index; - next[first] = { ...next[first], order: next[second].order ?? target }; - next[second] = { ...next[second], order: firstOrder }; - planning = next; - persist(); - render(); -}; const selectForPlacement = slug => { if (!unplanned().some(item => item.slug === slug)) return; selectedSlug = slug; @@ -80,7 +65,7 @@ const selectForPlacement = slug => { const renderUnplannedRecipe = item => { const active = item.slug === selectedSlug; const title = recipeTitle(item); - return `
+ return `
${esc(title)} @@ -89,13 +74,9 @@ const renderUnplannedRecipe = item => { const renderPlacedRecipe = item => { const title = recipeTitle(item); - const placement = planning[item.slug]; - const peers = slotItems(placement.date, placement.moment); - const index = peers.findIndex(peer => peer.slug === item.slug); - return `
+ return `
- - +
`; }; @@ -132,9 +113,13 @@ const render = () => { }; const activateUnplanned = card => { - if (ignoreClickSlug === card.dataset.selectPlanner) return; + if (ignoreClickSlug === card.dataset.selectPlanner) { ignoreClickSlug = null; return; } selectForPlacement(card.dataset.selectPlanner); }; +const activatePlaced = card => { + if (ignoreClickSlug === card.dataset.unplanCard) { ignoreClickSlug = null; return; } + unplan(card.dataset.unplanCard); +}; const activateSlot = slot => { if (!selectedSlug) return; place(selectedSlug, slot.dataset.slotDate, slot.dataset.slotMoment); @@ -149,18 +134,21 @@ const bindEvents = () => { }); }); document.querySelectorAll("[data-remove-selection]").forEach(button => button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); removeFromSelection(button.dataset.removeSelection); })); - document.querySelectorAll("[data-unplan]").forEach(button => button.addEventListener("click", event => { event.stopPropagation(); unplan(button.dataset.unplan); })); - document.querySelectorAll("[data-reorder]").forEach(button => button.addEventListener("click", event => { event.stopPropagation(); reorder(button.dataset.recipe, button.dataset.reorder === "up" ? -1 : 1); })); + document.querySelectorAll("[data-unplan-card]").forEach(card => { + card.addEventListener("click", () => activatePlaced(card)); + card.addEventListener("keydown", event => { + if (event.target !== card) return; + if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activatePlaced(card); } + }); + }); document.querySelectorAll("[data-planner-recipe]").forEach(card => { card.addEventListener("dragstart", event => { - draggingSlug = card.dataset.plannerRecipe; - event.dataTransfer.setData("text/plain", draggingSlug); + event.dataTransfer.setData("text/plain", card.dataset.plannerRecipe); event.dataTransfer.effectAllowed = "move"; card.classList.add("planner-recipe-dragging"); }); card.addEventListener("dragend", () => { - const slug = draggingSlug; - draggingSlug = null; + const slug = card.dataset.plannerRecipe; ignoreClickSlug = slug; card.classList.remove("planner-recipe-dragging"); window.setTimeout(() => { if (ignoreClickSlug === slug) ignoreClickSlug = null; }, 0); diff --git a/tests/test_issue_247.py b/tests/test_issue_247.py index ad16b71..519201d 100644 --- a/tests/test_issue_247.py +++ b/tests/test_issue_247.py @@ -11,14 +11,15 @@ def test_catalogue_has_the_shared_selection_entry_point(self): self.assertIn('data-selection-shopping hidden', source) self.assertIn('href="../meal-planner/"', source) - def test_placed_recipe_has_only_the_unplan_action(self): + def test_placed_recipe_is_the_unplan_action(self): source = (ROOT / "static/meal-planner/planner-app.js").read_text(encoding="utf-8") start = source.index("const renderPlacedRecipe") end = source.index("const renderSlot", start) placed = source[start:end] - self.assertIn("data-unplan", placed) + self.assertIn("data-unplan-card", placed) self.assertIn("Remettre", placed) self.assertIn("dans À placer", placed) + self.assertNotIn(" catalog.get(item.slug) || item", source) self.assertIn('class="planner-recipe-thumb"', source) - self.assertIn('draggable="true"', source) + self.assertIn("draggableAttribute", source) self.assertIn('import { buildCalendarExport } from "./calendar-export.js"', source) - self.assertIn('const reorder =', source) - self.assertIn('data-reorder="up"', source) - self.assertIn('data-unplan=', source) + self.assertNotIn('const reorder =', source) + self.assertNotIn('data-reorder="up"', source) + self.assertIn('data-unplan-card=', source) def test_thumbnail_cards_remain_accessible_and_responsive(self): - source = (ROOT / "static/meal-planner/style.css").read_text(encoding="utf-8") + source = (ROOT / "static/meal-planner/style.css").read_text(encoding="utf-8") + (ROOT / "static/meal-planner/direct-planner.css").read_text(encoding="utf-8") + direct_source = (ROOT / "static/meal-planner/direct-planner.css").read_text(encoding="utf-8") self.assertIn("cursor: grab", source) self.assertIn("cursor: grabbing", source) self.assertIn(".planner-recipe:hover", source) self.assertIn("@media (max-width: 760px)", source) self.assertIn(".planner-recipe-placed .planner-recipe-thumb", source) - self.assertIn(".planner-unplan", source) + self.assertNotIn(".planner-unplan", direct_source) if __name__ == "__main__":