TICKET-007 (frontend): save-as-meal flow, collapsible meal rows, unpack, meal component editor

This commit is contained in:
Craig
2026-07-26 17:27:15 +01:00
parent a8aed9a84f
commit 7db64840f7
9 changed files with 741 additions and 33 deletions
+133 -5
View File
@@ -2,13 +2,15 @@
// Dashboard — Daily view: progress bar + log entries grouped by meal_slot (spec §4.6).
// Every async view handles loading / error / empty states (spec §8.2 rule 4).
// Recent foods quick-log section per TICKET-006.
// Multi-select "Save as Meal" flow (spec §4.3, TICKET-007).
import { onMount } from 'svelte'
import { api } from '../lib/api.js'
import { currentDate, dayData, refreshDayData, appView, addLogEntryToStore } from '../lib/stores.svelte.js'
import { currentDate, dayData, refreshDayData, appView, replaceEntriesForMeal } from '../lib/stores.svelte.js'
import { formatDate, formatKcal, defaultQuantity } from '../lib/format.js'
import ProgressBar from './ProgressBar.svelte'
import LogEntry from './LogEntry.svelte'
import MealBuilder from './MealBuilder.svelte'
let { date } = $props()
@@ -48,7 +50,7 @@
meal_slot: getDefaultSlot(),
date: currentDate.value,
})
await addLogEntryToStore(entry)
dayData.log = [...dayData.log, entry]
} catch {
// Silently fail — user can manually log
}
@@ -86,6 +88,32 @@
if (unslotted.length) result.push({ slot: null, entries: unslotted })
return result
})
// ── Multi-select for "Save as Meal" (§4.3) ──────────────────────────────
let selectedIds = $state(new Set())
let selecting = $state(false) // true → checkboxes visible
let creatingMeal = $state(false) // true → show MealBuilder overlay
function toggleSelect(entryId) {
const next = new Set(selectedIds)
if (next.has(entryId)) {
next.delete(entryId)
} else {
next.add(entryId)
}
selectedIds = next
}
function cancelSelection() {
selecting = false
selectedIds = new Set()
}
function closeMealBuilder() {
creatingMeal = false
selecting = false
selectedIds = new Set()
}
</script>
<section class="dashboard">
@@ -126,17 +154,61 @@
{#if dayData.log.length === 0}
<p class="status empty">Nothing logged yet. Tap "Add Food" to get started.</p>
{:else}
{#if !selecting}
<div class="meal-actions">
<button type="button" class="secondary select-btn" onclick={() => selecting = true}>
☑ Select entries
</button>
</div>
{/if}
{#each groups as group (group.slot ?? 'other')}
<div class="meal-group">
<h3 class="slot-header">{group.slot || 'Other'}</h3>
<ul class="entry-list">
{#each group.entries as entry (entry.id)}
<LogEntry {entry} />
<li class="entry-row" class:selected={selectedIds.has(entry.id)}>
{#if selecting || creatingMeal}
<!-- Multi-select checkbox -->
<label class="select-label" title="Select for meal">
<input
type="checkbox"
checked={selectedIds.has(entry.id)}
onchange={() => toggleSelect(entry.id)}
class="select-checkbox"
/>
</label>
{/if}
<div class="entry-content">
<LogEntry {entry} />
</div>
</li>
{/each}
</ul>
</div>
{/each}
{/if}
<!-- Save as Meal button (spec §4.3) — shown when entries are selected -->
{#if selecting && !creatingMeal}
<div class="meal-actions">
<button type="button" class="save-meal-btn" onclick={() => creatingMeal = true} disabled={selectedIds.size < 2}>
🍽️ Save as Meal ({selectedIds.size} selected)
</button>
<button type="button" class="secondary" onclick={cancelSelection}>
Cancel selection
</button>
</div>
{/if}
<!-- MealBuilder overlay -->
{#if creatingMeal}
<MealBuilder
entryIds={[...selectedIds]}
date={currentDate.value}
onComplete={closeMealBuilder}
onCancel={closeMealBuilder}
/>
{/if}
{/if}
</section>
@@ -219,8 +291,64 @@
padding-bottom: 0.2rem;
border-bottom: 1px solid var(--border, #e5e7eb);
}
.entry-list {
/* ── Entry row with multi-select ────────────────────────────────────── */
.entry-row {
display: flex;
align-items: flex-start;
gap: 0.4rem;
margin: 0;
padding: 0;
}
</style>
.entry-row.selected {
background: var(--bg-muted, #f0f4ff);
border-radius: 0.35rem;
}
.select-label {
padding-top: 0.7rem;
flex-shrink: 0;
}
.select-checkbox {
width: 1.1rem;
height: 1.1rem;
cursor: pointer;
}
.entry-content {
flex: 1;
min-width: 0;
}
.entry-list {
margin: 0;
padding: 0;
list-style: none;
}
/* ── Meal actions ───────────────────────────────────────────────────── */
.meal-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
justify-content: center;
}
.save-meal-btn {
padding: 0.6rem 1.2rem;
background: #7c3aed;
color: #fff;
border: 1px solid #7c3aed;
border-radius: 0.35rem;
font: inherit;
font-size: 0.95rem;
cursor: pointer;
font-weight: 600;
}
button.secondary {
padding: 0.5rem 1rem;
border: 1px solid var(--border, #d1d5db);
border-radius: 0.35rem;
background: var(--bg, #fff);
cursor: pointer;
font: inherit;
font-size: 0.9rem;
}
</style>