Move UI to BoardUI with slate + salmon theme; add PDF plan export

- Adopt BoardUI (Pro init): theme tokens, Inter, .dark mode, Button,
  SegmentedControl, Meter, Skeleton and ThemeToggle; remap the app's
  color aliases onto BoardUI tokens and swap lucide for Remix icons
- Retint BoardUI's neutral ramp to cool slate with a salmon accent;
  drop the dotted background; raise muted text contrast
- Simplify the landing page and remove the overlap explorer
- Planner: explicit "I have this" toggle on grocery lines, pantry panel
  above the menu with the staples list spelled out, sticky aisle
  header fix, and a "Download PDF" plan export (jsPDF, lazy-loaded)
- Recipes: replace the family cabbage soup with Cade's Cabbage Soup
  (adds Bone-In Ham Steak to the catalog); remove Ham, Bean & Cabbage Soup
- Remove unused BoardUI components, dependencies and exports

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cayden Schwechten 2026-11-06 16:36:33 -06:00
parent b02c7a024b
commit df5dd3a70e
56 changed files with 4749 additions and 788 deletions

View file

@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test";
import { jsPDF } from "jspdf";
import { buildPlanModel, pdfSafe, renderPlanPdf } from "./exportPlan";
import { RECIPES } from "./recipes";
const breakfast = RECIPES.find((r) => r.kind === "breakfast")!;
const dinner = RECIPES.find((r) => r.kind === "main")!;
const state = {
daysCount: 2,
mealsPerDay: 3,
householdSize: 4,
// Monday breakfast + dinner, Tuesday dinner; "6-3" is outside daysCount and must not export.
slots: { "0-1": breakfast.id, "0-3": dinner.id, "1-3": dinner.id, "6-3": dinner.id },
};
describe("plan export", () => {
const model = buildPlanModel(state);
test("organizes meals under each planned day, in meal order", () => {
expect(model.days.map((d) => d.name)).toEqual(["Monday", "Tuesday"]);
const [monday, tuesday] = model.days;
expect(monday.meals.map((m) => ("empty" in m ? `${m.label}: -` : `${m.label}: ${m.title}`))).toEqual([
`Breakfast: ${breakfast.title}`,
"Lunch: -",
`Dinner: ${dinner.title}`,
]);
expect(tuesday.meals.filter((m) => !("empty" in m))).toHaveLength(1);
});
test("each meal carries its full recipe, scaled to the household", () => {
const meal = model.days[0].meals[0];
if ("empty" in meal) throw new Error("expected a planned meal");
expect(meal.steps).toEqual(breakfast.steps);
expect(meal.ingredients).toHaveLength(breakfast.ingredients.length);
expect(meal.meta).toContain("serves 4");
});
test("text is made printable in the base PDF font", () => {
expect(pdfSafe("1⅓ cup flour")).toBe("1 1/3 cup flour");
expect(pdfSafe("⅛ tsp salt")).toBe("1/8 tsp salt");
expect(pdfSafe("½ cup · 350°F — Rakı")).toBe("½ cup · 350°F — Raki");
});
test("renders a real multi-page PDF (one day per page)", () => {
const bytes = new Uint8Array(renderPlanPdf(model, jsPDF).output("arraybuffer"));
expect(new TextDecoder().decode(bytes.slice(0, 5))).toBe("%PDF-");
expect(renderPlanPdf(model, jsPDF).getNumberOfPages()).toBeGreaterThanOrEqual(2);
});
});