Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 2 additions & 33 deletions static/meal-planner/direct-planner.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
52 changes: 20 additions & 32 deletions static/meal-planner/planner-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)); };
Expand Down Expand Up @@ -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;
Expand All @@ -80,7 +65,7 @@ const selectForPlacement = slug => {
const renderUnplannedRecipe = item => {
const active = item.slug === selectedSlug;
const title = recipeTitle(item);
return `<article class="planner-recipe planner-recipe-unplaced${active ? " planner-recipe-selected" : ""}" draggable="true" tabindex="0" role="button" aria-pressed="${active}" aria-label="Sélectionner ${esc(title)} pour le placement" title="${esc(title)}" data-planner-recipe="${esc(item.slug)}" data-select-planner="${esc(item.slug)}">
return `<article class="planner-recipe planner-recipe-unplaced${active ? " planner-recipe-selected" : ""}"${draggableAttribute()} tabindex="0" role="button" aria-pressed="${active}" aria-label="Sélectionner ${esc(title)} pour le placement" title="${esc(title)}" data-planner-recipe="${esc(item.slug)}" data-select-planner="${esc(item.slug)}">
<span class="planner-recipe-thumb" aria-hidden="true">${recipeImage(item)}</span>
<span class="planner-recipe-title">${esc(title)}</span>
<button type="button" class="planner-selection-remove" data-remove-selection="${esc(item.slug)}" aria-label="Retirer ${esc(title)} de Ma sélection" title="Retirer de Ma sélection">×</button>
Expand All @@ -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 `<article class="planner-recipe planner-recipe-placed" draggable="true" tabindex="0" aria-label="${esc(title)}" title="${esc(title)}" data-planner-recipe="${esc(item.slug)}">
return `<article class="planner-recipe planner-recipe-placed"${draggableAttribute()} tabindex="0" role="button" aria-label="Remettre ${esc(title)} dans À placer" title="${esc(title)} — cliquer pour remettre dans À placer" data-planner-recipe="${esc(item.slug)}" data-unplan-card="${esc(item.slug)}">
<span class="planner-recipe-thumb" aria-hidden="true">${recipeImage(item)}</span>
<span class="planner-recipe-overlay" aria-hidden="true"><strong>${esc(title)}</strong></span>
<span class="planner-recipe-actions"><button type="button" class="planner-order" data-reorder="up" data-recipe="${esc(item.slug)}" aria-label="Monter ${esc(title)}" ${index === 0 ? "disabled" : ""}>↑</button><button type="button" class="planner-order" data-reorder="down" data-recipe="${esc(item.slug)}" aria-label="Descendre ${esc(title)}" ${index === peers.length - 1 ? "disabled" : ""}>↓</button><button type="button" class="planner-unplan" data-unplan="${esc(item.slug)}" aria-label="Remettre ${esc(title)} dans À placer" title="Remettre dans À placer">×</button></span>
<span class="planner-recipe-overlay" aria-hidden="true"><strong>${esc(title)}</strong><span>↩ À placer</span></span>
</article>`;
};

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions tests/test_issue_247.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ 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("<button", placed)
self.assertNotIn("data-remove-selection", placed)
self.assertNotIn("planner-recipe-menu", placed)

def test_planner_state_and_calendar_hooks_remain(self):
source = (ROOT / "static/meal-planner/planner-app.js").read_text(encoding="utf-8")
self.assertIn('import { addPlacement, loadPlanning, MOMENTS, removePlacement, savePlanning }', source)
self.assertIn('import { buildCalendarExport }', source)
self.assertIn('draggable="true"', source)
self.assertIn("draggableAttribute", source)


if __name__ == "__main__":
Expand Down
8 changes: 6 additions & 2 deletions tests/test_issue_254.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class Issue254PlannerDirectGestureTests(unittest.TestCase):
def test_planner_uses_direct_recipe_and_slot_controls(self):
source = (ROOT / "static/meal-planner/planner-app.js").read_text(encoding="utf-8")
self.assertIn("data-select-planner", source)
self.assertIn("data-unplan=", source)
self.assertNotIn("data-unplan-card", source)
self.assertIn("data-unplan-card", source)
self.assertNotIn("data-unplan=", source)
self.assertIn("planner-slot-ready", source)
self.assertIn("selectedSlug", source)
self.assertNotIn("data-slot-add", source)
Expand All @@ -25,7 +25,10 @@ def test_drag_and_click_paths_are_kept_separate(self):
self.assertIn("dragend", source)
self.assertIn("ignoreClickSlug", source)
self.assertIn('event.dataTransfer.effectAllowed = "move"', source)
self.assertIn("activatePlaced", source)
self.assertIn("activateSlot", source)
self.assertIn('window.matchMedia("(pointer: coarse)")', source)
self.assertIn("draggableAttribute", source)

def test_unplaced_removal_is_isolated_from_placement(self):
source = (ROOT / "static/meal-planner/planner-app.js").read_text(encoding="utf-8")
Expand Down Expand Up @@ -63,6 +66,7 @@ def test_direct_planner_styles_make_thumbnail_primary(self):
self.assertIn(".planner-board-shell.planner-board-shell-full", source)
self.assertIn(".planner-selection-remove", source)
self.assertIn("grid-template-columns: 64px minmax(0, 1fr) 44px", source)
self.assertNotIn(".planner-unplan", source)
self.assertIn('@media (max-width: 760px)', source)


Expand Down
13 changes: 7 additions & 6 deletions tests/test_planner_thumbnails.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@ def test_planner_enriches_selection_from_catalog_without_changing_state(self):
self.assertIn("let catalog = new Map()", source)
self.assertIn("const recipeFor = item => 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__":
Expand Down
Loading