Meal Planner
Weekly meal plan built on Planner — meals bucketed by day, sorted by meal type, with an add affordance that lives on every day including empty ones.
Live Preview
Week 25 – June 2026
Week 25 – June 2026
Key Features
- Planner buckets a typed MealEntry[] by local day via getDate — no manual date math
- sort orders meals within a day (breakfast → lunch → dinner)
- The cell snippet renders your own markup and runs for empty days, so "Add" is always reachable
- onNavigate hands you the visible week range to load data per week
- bind:selectedDate tracks the active day; clicking a cell body selects it
- Server and client agree on the week via the Svelte-free @urbicon-ui/blocks/date subpath
Code
Meal Planner Recipe
<script lang="ts">
import { Planner, Button, Badge, PlusIcon } from '@urbicon-ui/blocks';
import { endOfWeek, toIso } from '@urbicon-ui/blocks/date';
type MealType = 'breakfast' | 'lunch' | 'dinner';
interface MealEntry { id: string; date: string; mealType: MealType; title: string; emoji: string; }
const MEAL_ORDER: Record<MealType, number> = { breakfast: 0, lunch: 1, dinner: 2 };
let entries = $state<MealEntry[]>(initialEntries);
let referenceDate = $state(new Date());
let selectedDate = $state<Date | undefined>();
// Fetch a week's worth of entries whenever navigation moves the window.
async function loadWeek(start: Date) {
entries = await db.meals.between(toIso(start), toIso(endOfWeek(start, 1)));
}
function addMeal(isoDate: string) {
entries.push({ id: crypto.randomUUID(), date: isoDate, mealType: 'lunch', title: '', emoji: '🍽️' });
}
</script>
<Planner
view="week"
items={entries}
getDate={(e) => e.date}
sort={(a, b) => MEAL_ORDER[a.mealType] - MEAL_ORDER[b.mealType]}
bind:value={referenceDate}
bind:selectedDate
onNavigate={(_, range) => loadWeek(range.start)}
>
{#snippet cell({ items, isoDate })}
{#each items as meal (meal.id)}
<div class="bg-surface-subtle flex items-center gap-2 rounded-md px-2 py-1.5">
<span aria-hidden="true">{meal.emoji}</span>
<span class="text-text-secondary truncate text-sm">{meal.title}</span>
</div>
{/each}
<!-- cell runs for empty days too → Add is reachable everywhere -->
<Button variant="ghost" size="sm" class="mt-auto justify-start" onclick={() => addMeal(isoDate)}>
<PlusIcon size={14} /> Add
</Button>
{/snippet}
</Planner>