From a482bf5b09f3d40747f8afe42245de0b5ffd5d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffi=20M=C3=BCller?= Date: Tue, 4 Aug 2026 12:22:32 +0200 Subject: [PATCH] Add Phase 1 Pocket Pascal UI: home, pantry, week plan, and plate. Replace the hello-world shell with the elevated design system, bilingual screens, recipe/extras data, and the interactive Mein Teller view. Co-authored-by: Cursor --- client/index.html | 9 +- client/package.json | 2 + client/public/fonts/albert-sans-400.woff2 | Bin 0 -> 10020 bytes client/public/fonts/albert-sans-500.woff2 | Bin 0 -> 10344 bytes client/public/fonts/albert-sans-700.woff2 | Bin 0 -> 10356 bytes client/public/fonts/unbounded-700.woff2 | Bin 0 -> 21212 bytes client/public/fonts/unbounded-900.woff2 | Bin 0 -> 21084 bytes client/src/App.tsx | 84 +- client/src/components/home/BuilderSlot.tsx | 43 + client/src/components/home/HomeScreen.tsx | 133 +++ client/src/components/home/KnowledgeHub.tsx | 79 ++ client/src/components/home/SuggestionCard.tsx | 128 +++ client/src/components/layout/AppShell.tsx | 53 + client/src/components/plate/DiningPlate.tsx | 182 ++++ client/src/components/ui-pp/Accordion.tsx | 168 +++ client/src/components/ui-pp/BottomNav.tsx | 98 ++ client/src/components/ui-pp/Button.tsx | 115 ++ client/src/components/ui-pp/GlassCard.tsx | 48 + client/src/components/ui-pp/SegControl.tsx | 62 ++ client/src/data/extras.json | 14 + client/src/data/foods.json | 979 ++++++++++++++++++ client/src/data/recipes.json | 160 +++ client/src/data/steps.json | 66 ++ client/src/i18n/ui.ts | 148 +++ client/src/index.css | 18 +- client/src/lib/catalog.ts | 139 +++ client/src/lib/cx.ts | 4 + client/src/lib/language.tsx | 65 ++ client/src/lib/suggestion.ts | 103 ++ client/src/lib/timeContext.ts | 36 + client/src/lib/widgetOrder.ts | 55 + client/src/screens/BuilderScreen.tsx | 53 + client/src/screens/PantryScreen.tsx | 124 +++ client/src/screens/PlateScreen.tsx | 283 +++++ client/src/screens/SettingsScreen.tsx | 58 ++ client/src/screens/WeekPlanScreen.tsx | 136 +++ client/src/styles/glass.css | 29 + client/src/styles/theme.css | 270 +++++ client/src/types/domain.ts | 79 ++ client/tsconfig.app.json | 1 + client/vite.config.ts | 6 +- handover/CLAUDE.md | 43 + handover/CONCEPT.html | 665 ++++++++++++ .../component-library/ComponentShowcase.tsx | 112 ++ handover/component-library/README.md | 58 ++ .../components/BottomNav.tsx | 88 ++ .../component-library/components/Button.tsx | 112 ++ .../components/GlassCard.tsx | 62 ++ .../components/PlateChart.tsx | 121 +++ handover/component-library/lib/cx.ts | 4 + .../public/fonts/albert-sans-400.woff2 | Bin 0 -> 10020 bytes .../public/fonts/albert-sans-500.woff2 | Bin 0 -> 10344 bytes .../public/fonts/albert-sans-700.woff2 | Bin 0 -> 10356 bytes .../public/fonts/unbounded-700.woff2 | Bin 0 -> 21212 bytes .../public/fonts/unbounded-900.woff2 | Bin 0 -> 21084 bytes handover/component-library/styles/theme.css | 192 ++++ handover/data/foods.json | 979 ++++++++++++++++++ handover/data/steps.json | 66 ++ handover/reference-kueche_app.html | 836 +++++++++++++++ pnpm-lock.yaml | 70 ++ 60 files changed, 7366 insertions(+), 72 deletions(-) create mode 100644 client/public/fonts/albert-sans-400.woff2 create mode 100644 client/public/fonts/albert-sans-500.woff2 create mode 100644 client/public/fonts/albert-sans-700.woff2 create mode 100644 client/public/fonts/unbounded-700.woff2 create mode 100644 client/public/fonts/unbounded-900.woff2 create mode 100644 client/src/components/home/BuilderSlot.tsx create mode 100644 client/src/components/home/HomeScreen.tsx create mode 100644 client/src/components/home/KnowledgeHub.tsx create mode 100644 client/src/components/home/SuggestionCard.tsx create mode 100644 client/src/components/layout/AppShell.tsx create mode 100644 client/src/components/plate/DiningPlate.tsx create mode 100644 client/src/components/ui-pp/Accordion.tsx create mode 100644 client/src/components/ui-pp/BottomNav.tsx create mode 100644 client/src/components/ui-pp/Button.tsx create mode 100644 client/src/components/ui-pp/GlassCard.tsx create mode 100644 client/src/components/ui-pp/SegControl.tsx create mode 100644 client/src/data/extras.json create mode 100644 client/src/data/foods.json create mode 100644 client/src/data/recipes.json create mode 100644 client/src/data/steps.json create mode 100644 client/src/i18n/ui.ts create mode 100644 client/src/lib/catalog.ts create mode 100644 client/src/lib/cx.ts create mode 100644 client/src/lib/language.tsx create mode 100644 client/src/lib/suggestion.ts create mode 100644 client/src/lib/timeContext.ts create mode 100644 client/src/lib/widgetOrder.ts create mode 100644 client/src/screens/BuilderScreen.tsx create mode 100644 client/src/screens/PantryScreen.tsx create mode 100644 client/src/screens/PlateScreen.tsx create mode 100644 client/src/screens/SettingsScreen.tsx create mode 100644 client/src/screens/WeekPlanScreen.tsx create mode 100644 client/src/styles/glass.css create mode 100644 client/src/styles/theme.css create mode 100644 client/src/types/domain.ts create mode 100644 handover/CLAUDE.md create mode 100644 handover/CONCEPT.html create mode 100644 handover/component-library/ComponentShowcase.tsx create mode 100644 handover/component-library/README.md create mode 100644 handover/component-library/components/BottomNav.tsx create mode 100644 handover/component-library/components/Button.tsx create mode 100644 handover/component-library/components/GlassCard.tsx create mode 100644 handover/component-library/components/PlateChart.tsx create mode 100644 handover/component-library/lib/cx.ts create mode 100644 handover/component-library/public/fonts/albert-sans-400.woff2 create mode 100644 handover/component-library/public/fonts/albert-sans-500.woff2 create mode 100644 handover/component-library/public/fonts/albert-sans-700.woff2 create mode 100644 handover/component-library/public/fonts/unbounded-700.woff2 create mode 100644 handover/component-library/public/fonts/unbounded-900.woff2 create mode 100644 handover/component-library/styles/theme.css create mode 100644 handover/data/foods.json create mode 100644 handover/data/steps.json create mode 100644 handover/reference-kueche_app.html diff --git a/client/index.html b/client/index.html index e4e9f57..39a9c7a 100644 --- a/client/index.html +++ b/client/index.html @@ -1,5 +1,5 @@ - + @@ -7,14 +7,17 @@ name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> - + - + diff --git a/client/package.json b/client/package.json index 0649833..e514968 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "lucide-react": "^1.28.0", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", "shadcn": "^4.16.1", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.3", @@ -29,6 +30,7 @@ "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^6.0.4", "oxlint": "^1.75.0", "typescript": "~6.0.2", diff --git a/client/public/fonts/albert-sans-400.woff2 b/client/public/fonts/albert-sans-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0c8c5dec4da456966f7315d382de122dc0853ff0 GIT binary patch literal 10020 zcmV+4*&oF0AK6?04Br$0RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/client/public/fonts/albert-sans-500.woff2 b/client/public/fonts/albert-sans-500.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3327874b2532089f4544e3b665050e2827de5ca6 GIT binary patch literal 10344 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 GphW<%!~E$0 literal 0 HcmV?d00001 diff --git a/client/public/fonts/albert-sans-700.woff2 b/client/public/fonts/albert-sans-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b53856a3a8adc67f0bec6b9acd3e2f9541c4f553 GIT binary patch literal 10356 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 G=zjq^*5HEx literal 0 HcmV?d00001 diff --git a/client/public/fonts/unbounded-700.woff2 b/client/public/fonts/unbounded-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a77b17c8ebaead07f3a76a13c4ba3b30f815bd4f GIT binary patch literal 21212 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 z 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 literal 0 HcmV?d00001 diff --git a/client/src/App.tsx b/client/src/App.tsx index 8401d34..3e6f7fc 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,66 +1,30 @@ -import { useCallback, useEffect, useState } from "react" -import { RefreshCw } from "lucide-react" +import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom" -import { Button } from "@/components/ui/button" -import { fetchHello, type HelloResponse } from "@/lib/api" +import { HomeScreen } from "@/components/home/HomeScreen" +import { AppShell } from "@/components/layout/AppShell" +import { LanguageProvider } from "@/lib/language" +import { BuilderScreen } from "@/screens/BuilderScreen" +import { PantryScreen } from "@/screens/PantryScreen" +import { PlateScreen } from "@/screens/PlateScreen" +import { SettingsScreen } from "@/screens/SettingsScreen" +import { WeekPlanScreen } from "@/screens/WeekPlanScreen" export default function App() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [loading, setLoading] = useState(true) - - const load = useCallback(async () => { - setLoading(true) - setError(null) - try { - setData(await fetchHello()) - } catch (err) { - setError(err instanceof Error ? err.message : "Something went wrong") - } finally { - setLoading(false) - } - }, []) - - useEffect(() => { - void load() - }, [load]) - return ( -
-
- - Pocket Pascal - - -

- {data?.message ?? "Hello, World!"} -

- -

- A minimal installable PWA. React + shadcn/ui on the front, Express + - SQLite on the back. -

- -
- {loading ? ( - Asking the server… - ) : error ? ( - {error} - ) : ( - <> - - {data?.visits.toLocaleString()} - - times this hello has been said - - )} -
- - -
-
+ + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ) } diff --git a/client/src/components/home/BuilderSlot.tsx b/client/src/components/home/BuilderSlot.tsx new file mode 100644 index 0000000..bf1299e --- /dev/null +++ b/client/src/components/home/BuilderSlot.tsx @@ -0,0 +1,43 @@ +import { useNavigate } from "react-router-dom" +import { ChefHat } from "lucide-react" + +import { GlassCard } from "@/components/ui-pp/GlassCard" +import { t, ui } from "@/i18n/ui" +import { useLanguage } from "@/lib/language" + +/** Navigation tile — whole card is the hit target (same pattern as KnowledgeHub). */ +export function BuilderSlot() { + const { lang } = useLanguage() + const navigate = useNavigate() + + return ( + navigate("/builder")} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + navigate("/builder") + } + }} + > +
+ + + +
+

+ {t(ui.builderSlot.title, lang)} +

+

+ {t(ui.builderSlot.body, lang)} +

+
+
+
+ ) +} diff --git a/client/src/components/home/HomeScreen.tsx b/client/src/components/home/HomeScreen.tsx new file mode 100644 index 0000000..2cdd3b8 --- /dev/null +++ b/client/src/components/home/HomeScreen.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState, type ReactNode } from "react" +import { ChevronDown, ChevronUp } from "lucide-react" + +import { BuilderSlot } from "@/components/home/BuilderSlot" +import { KnowledgeHub } from "@/components/home/KnowledgeHub" +import { SuggestionCard } from "@/components/home/SuggestionCard" +import { t, ui } from "@/i18n/ui" +import { useLanguage } from "@/lib/language" +import { cx } from "@/lib/cx" +import { + DEFAULT_WIDGET_ORDER, + moveWidget, + readWidgetOrder, + writeWidgetOrder, +} from "@/lib/widgetOrder" +import type { HomeWidgetId, WidgetOrder } from "@/types/domain" + +function WidgetShell({ + id, + editing, + order, + onMove, + children, +}: { + id: HomeWidgetId + editing: boolean + order: WidgetOrder + onMove: (id: HomeWidgetId, direction: "up" | "down") => void + children: ReactNode +}) { + const { lang } = useLanguage() + const index = order.indexOf(id) + + return ( +
+ {editing ? ( +
+ + +
+ ) : null} + {children} +
+ ) +} + +export function HomeScreen() { + const { lang } = useLanguage() + const [editing, setEditing] = useState(false) + const [order, setOrder] = useState(DEFAULT_WIDGET_ORDER) + + useEffect(() => { + setOrder(readWidgetOrder()) + }, []) + + function handleMove(id: HomeWidgetId, direction: "up" | "down") { + setOrder((current) => { + const next = moveWidget(current, id, direction) + writeWidgetOrder(next) + return next + }) + } + + const widgets: Record = { + suggestion: , + knowledge: , + builder: , + } + + return ( +
+
+
+

Pocket Pascal

+

+ {t(ui.home.title, lang)} +

+
+ +
+ + {order.map((id) => ( + + {widgets[id]} + + ))} +
+ ) +} diff --git a/client/src/components/home/KnowledgeHub.tsx b/client/src/components/home/KnowledgeHub.tsx new file mode 100644 index 0000000..5ec902e --- /dev/null +++ b/client/src/components/home/KnowledgeHub.tsx @@ -0,0 +1,79 @@ +import { useNavigate } from "react-router-dom" +import { BookOpen, CalendarDays, Refrigerator } from "lucide-react" +import type { ReactNode } from "react" + +import { GlassCard } from "@/components/ui-pp/GlassCard" +import { t, ui } from "@/i18n/ui" +import { useLanguage } from "@/lib/language" + +function HubTile({ + title, + body, + gradient, + icon, + onNavigate, +}: { + title: string + body: string + gradient: string + icon: ReactNode + onNavigate: () => void +}) { + return ( + { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + onNavigate() + } + }} + > +
+ + {icon} + +
+

{title}

+

{body}

+
+
+
+ ) +} + +export function KnowledgeHub() { + const { lang } = useLanguage() + const navigate = useNavigate() + + return ( +
+ } + onNavigate={() => navigate("/knowledge")} + /> + } + onNavigate={() => navigate("/pantry")} + /> + } + onNavigate={() => navigate("/weekplan")} + /> +
+ ) +} diff --git a/client/src/components/home/SuggestionCard.tsx b/client/src/components/home/SuggestionCard.tsx new file mode 100644 index 0000000..0ec7904 --- /dev/null +++ b/client/src/components/home/SuggestionCard.tsx @@ -0,0 +1,128 @@ +import { useMemo, useState } from "react" +import { useNavigate } from "react-router-dom" +import { Pin, RefreshCw } from "lucide-react" + +import { Button } from "@/components/ui-pp/Button" +import { GlassCard } from "@/components/ui-pp/GlassCard" +import { t, tr, ui } from "@/i18n/ui" +import { useLanguage } from "@/lib/language" +import { cx } from "@/lib/cx" +import { buildSuggestion, reshuffleSuggestion } from "@/lib/suggestion" +import { formatClock, getTimeContext } from "@/lib/timeContext" +import type { FoodItem, Recipe, TimeSlot } from "@/types/domain" + +const CAT_DOT: Record = { + protein: "var(--color-protein)", + obst: "var(--color-obstgem)", + gemuese: "var(--color-obstgem)", + fett: "var(--color-fett)", + kh: "var(--color-kh)", + snack: "var(--color-protein)", +} + +function slotLabel(slot: TimeSlot, lang: "de" | "en"): string { + return t(ui.suggestion[slot], lang) +} + +export function SuggestionCard() { + const { lang } = useLanguage() + const navigate = useNavigate() + const context = useMemo(() => getTimeContext(), []) + + const [suggestion, setSuggestion] = useState(() => buildSuggestion(context.slot)) + const [pinnedIds, setPinnedIds] = useState>(() => new Set()) + + const recipe: Recipe | null = suggestion.recipe + const items: FoodItem[] = suggestion.foods + + const clock = formatClock(context.hour, context.minute, lang) + const header = + lang === "de" + ? `${clock} ${t(ui.suggestion.timeSuffix, lang)} · ${slotLabel(context.slot, lang)}` + : `${clock} · ${slotLabel(context.slot, lang)}` + + const title = recipe ? tr(recipe.name, lang) : t(ui.suggestion.fallbackName, lang) + + function togglePin(id: string) { + setPinnedIds((current) => { + const next = new Set(current) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } + + function reroll() { + if (pinnedIds.size === 0) { + setSuggestion(buildSuggestion(context.slot, recipe?.id)) + return + } + setSuggestion({ + recipe: null, + foods: reshuffleSuggestion(items, pinnedIds, context.slot), + }) + } + + function sendToBuilder() { + navigate("/builder", { + state: { + ingredientIds: items.map((item) => item.id), + source: "suggestion" as const, + }, + }) + } + + return ( + +
+
+

{header}

+

{title}

+
+ +
+ {items.map((item) => { + const pinned = pinnedIds.has(item.id) + return ( + + ) + })} +
+ +
+ + +
+
+
+ ) +} diff --git a/client/src/components/layout/AppShell.tsx b/client/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..20ba571 --- /dev/null +++ b/client/src/components/layout/AppShell.tsx @@ -0,0 +1,53 @@ +import { Outlet, useLocation, useNavigate } from "react-router-dom" +import { BookOpen, Home, Settings2, ShoppingBasket } from "lucide-react" + +import { BottomNav } from "@/components/ui-pp/BottomNav" +import { t, ui } from "@/i18n/ui" +import { useLanguage } from "@/lib/language" + +const ROUTES = [ + { key: "home", path: "/", icon: Home, labelKey: "home" as const }, + { key: "pantry", path: "/pantry", icon: ShoppingBasket, labelKey: "pantry" as const }, + { key: "knowledge", path: "/knowledge", icon: BookOpen, labelKey: "knowledge" as const }, + { key: "settings", path: "/settings", icon: Settings2, labelKey: "settings" as const }, +] + +export function AppShell() { + const { lang } = useLanguage() + const location = useLocation() + const navigate = useNavigate() + + const items = ROUTES.map((route) => { + const Icon = route.icon + const active = + route.path === "/" + ? location.pathname === "/" + : location.pathname.startsWith(route.path) + + return { + key: route.key, + label: t(ui.nav[route.labelKey], lang), + active, + icon: , + onClick: () => navigate(route.path), + } + }) + + return ( +
+
+ +
+ +
+ ) +} diff --git a/client/src/components/plate/DiningPlate.tsx b/client/src/components/plate/DiningPlate.tsx new file mode 100644 index 0000000..df6047c --- /dev/null +++ b/client/src/components/plate/DiningPlate.tsx @@ -0,0 +1,182 @@ +import { useId } from "react" + +import { cx } from "@/lib/cx" + +export interface PlateSegment { + id: string + /** Share of the plate (relative weights; need not sum to 1). */ + weight: number + color: string + label: string +} + +export interface DiningPlateProps { + segments: PlateSegment[] + size?: number + onSelect?: (id: string) => void + className?: string + "aria-label"?: string +} + +function polar(centerX: number, centerY: number, r: number, angleDeg: number) { + const rad = ((angleDeg - 90) * Math.PI) / 180 + return { x: centerX + r * Math.cos(rad), y: centerY + r * Math.sin(rad) } +} + +/** Filled pie wedge from center (food on the plate — not a thin outer ring). */ +function wedgePath( + centerX: number, + centerY: number, + r: number, + startAngle: number, + endAngle: number, +): string { + if (endAngle - startAngle >= 359.9) { + return [ + `M ${centerX} ${centerY - r}`, + `A ${r} ${r} 0 1 1 ${centerX} ${centerY + r}`, + `A ${r} ${r} 0 1 1 ${centerX} ${centerY - r}`, + "Z", + ].join(" ") + } + const start = polar(centerX, centerY, r, startAngle) + const end = polar(centerX, centerY, r, endAngle) + const large = endAngle - startAngle > 180 ? 1 : 0 + return [ + `M ${centerX} ${centerY}`, + `L ${start.x} ${start.y}`, + `A ${r} ${r} 0 ${large} 1 ${end.x} ${end.y}`, + "Z", + ].join(" ") +} + +/** + * Ceramic dining plate with food-like category wedges inside the rim. + * Rim depth via CSS inset shadows; wedges are SVG (clickable). + */ +export function DiningPlate({ + segments, + size = 280, + onSelect, + className, + "aria-label": ariaLabel, +}: DiningPlateProps) { + const uid = useId().replace(/:/g, "") + const centerX = size / 2 + const centerY = size / 2 + const rimPad = size * 0.09 + const foodR = size / 2 - rimPad + const gapDeg = 2.5 + + const total = segments.reduce((sum, s) => sum + s.weight, 0) || 1 + let angle = 0 + const wedges = segments.map((segment) => { + const sweep = (segment.weight / total) * 360 + const start = angle + gapDeg / 2 + const end = angle + sweep - gapDeg / 2 + angle += sweep + return { + ...segment, + start, + end, + d: end > start ? wedgePath(centerX, centerY, foodR, start, end) : null, + } + }) + + return ( +
+ {/* Ceramic body + raised rim */} +
+ + {/* Soft recessed well under the food */} +
+ + + + + + + + + + + + + + + + + + {wedges.map( + (wedge) => + wedge.d && ( + onSelect?.(wedge.id)} + onKeyDown={(event) => { + if (!onSelect) return + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + onSelect(wedge.id) + } + }} + /> + ), + )} + + + {/* Subtle inner rim line separating food from ceramic lip */} + + +
+ ) +} diff --git a/client/src/components/ui-pp/Accordion.tsx b/client/src/components/ui-pp/Accordion.tsx new file mode 100644 index 0000000..affb3a9 --- /dev/null +++ b/client/src/components/ui-pp/Accordion.tsx @@ -0,0 +1,168 @@ +import { useId, useState, type ReactNode } from "react" +import { ChevronRight } from "lucide-react" + +import { cx } from "@/lib/cx" + +export interface AccordionProps { + title: ReactNode + meta?: ReactNode + icon?: ReactNode + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + className?: string + children: ReactNode +} + +/** + * Top-level elevated accordion (type groups, pantry categories). + * Bevel/fill use rounded-[inherit] so radius tokens stay consistent. + */ +export function Accordion({ + title, + meta, + icon, + defaultOpen = false, + open: controlledOpen, + onOpenChange, + className, + children, +}: AccordionProps) { + const reactId = useId() + const panelId = `acc-panel-${reactId}` + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen) + const isControlled = controlledOpen !== undefined + const open = isControlled ? controlledOpen : uncontrolledOpen + + function toggle() { + const next = !open + if (!isControlled) setUncontrolledOpen(next) + onOpenChange?.(next) + } + + return ( +
+
+
+
+ + + + {open ? ( +
+ {children} +
+ ) : null} +
+
+ ) +} + +export interface NestedDisclosureProps { + title: ReactNode + defaultOpen?: boolean + className?: string + children: ReactNode +} + +/** + * Compact nested row for lists inside an Accordion. + * Radius --radius-nested with parent p-3 → outer card 24 = 12 + 12. + */ +export function NestedDisclosure({ + title, + defaultOpen = false, + className, + children, +}: NestedDisclosureProps) { + const reactId = useId() + const panelId = `nested-panel-${reactId}` + const [open, setOpen] = useState(defaultOpen) + + return ( +
+ + {open ? ( +
+ {children} +
+ ) : null} +
+ ) +} diff --git a/client/src/components/ui-pp/BottomNav.tsx b/client/src/components/ui-pp/BottomNav.tsx new file mode 100644 index 0000000..9d50c6f --- /dev/null +++ b/client/src/components/ui-pp/BottomNav.tsx @@ -0,0 +1,98 @@ +import type { ReactNode } from "react" +import { cx } from "@/lib/cx" + +export interface NavItem { + key: string + icon: ReactNode + label: string + active?: boolean + onClick?: () => void + href?: string +} + +export interface BottomNavProps { + items: NavItem[] + className?: string +} + +/** + * Floating pill bottom navigation — float elevation above content cards. + */ +export function BottomNav({ items, className }: BottomNavProps) { + return ( + + ) +} diff --git a/client/src/components/ui-pp/Button.tsx b/client/src/components/ui-pp/Button.tsx new file mode 100644 index 0000000..07e3b96 --- /dev/null +++ b/client/src/components/ui-pp/Button.tsx @@ -0,0 +1,115 @@ +import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react" +import { cx } from "@/lib/cx" + +export type ButtonVariant = "primary" | "secondary" | "fab" + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant + loading?: boolean + /** Required for `variant="fab"` — icon-only, no visible label. */ + icon?: ReactNode +} + +function Spinner({ dark = false }: { dark?: boolean }) { + return ( + + ) +} + +const pressable = cx( + "elevation-transition active:translate-y-px active:brightness-95", + "shadow-[var(--shadow-elevated)] active:shadow-[var(--shadow-pressed)]", +) + +const base = cx( + "relative inline-flex items-center justify-center gap-2", + "type-button no-select [-webkit-tap-highlight-color:transparent]", + "disabled:opacity-40 disabled:pointer-events-none disabled:active:translate-y-0 disabled:active:shadow-[var(--shadow-elevated)]", +) + +/** + * Touch-optimized button set. All variants meet the 48px minimum touch target. + * Text on gradients uses --ink for WCAG AA on light brand hues. + */ +export const Button = forwardRef( + ( + { variant = "primary", loading = false, disabled, icon, className, children, ...props }, + ref, + ) => { + const isDisabled = disabled || loading + + if (variant === "fab") { + return ( + + ) + } + + if (variant === "secondary") { + return ( + + ) + } + + return ( + + ) + }, +) +Button.displayName = "Button" diff --git a/client/src/components/ui-pp/GlassCard.tsx b/client/src/components/ui-pp/GlassCard.tsx new file mode 100644 index 0000000..c2116e8 --- /dev/null +++ b/client/src/components/ui-pp/GlassCard.tsx @@ -0,0 +1,48 @@ +import { forwardRef, type HTMLAttributes } from "react" +import { cx } from "@/lib/cx" + +export interface GlassCardProps extends HTMLAttributes { + /** + * `default` — static, sits at rest elevation. + * `interactive` — settles down 1px + softens shadow on press. + */ + variant?: "default" | "interactive" +} + +/** + * Elevated Surface Card — primary container. + * Radius via --radius-card; bevel inherits so nested overrides stay consistent. + */ +export const GlassCard = forwardRef( + ({ variant = "default", className, children, ...props }, ref) => { + const interactive = variant === "interactive" + return ( +
+
+
+
{children}
+
+ ) + }, +) +GlassCard.displayName = "GlassCard" diff --git a/client/src/components/ui-pp/SegControl.tsx b/client/src/components/ui-pp/SegControl.tsx new file mode 100644 index 0000000..659a5d2 --- /dev/null +++ b/client/src/components/ui-pp/SegControl.tsx @@ -0,0 +1,62 @@ +import { cx } from "@/lib/cx" + +export interface SegOption { + value: T + label: string +} + +export interface SegControlProps { + value: T + options: SegOption[] + onChange: (value: T) => void + className?: string + /** Accessible name for the control group. */ + "aria-label"?: string +} + +/** + * Segmented control — same interaction pattern as the prototype `.segctl`. + * Soft track + elevated active pill. Touch targets ≥48px. + */ +export function SegControl({ + value, + options, + onChange, + className, + "aria-label": ariaLabel, +}: SegControlProps) { + return ( +
+ {options.map((option) => { + const active = option.value === value + return ( + + ) + })} +
+ ) +} diff --git a/client/src/data/extras.json b/client/src/data/extras.json new file mode 100644 index 0000000..7d30fe7 --- /dev/null +++ b/client/src/data/extras.json @@ -0,0 +1,14 @@ +[ + { "id": "soy_sauce", "name": { "de": "Sojasauce", "en": "Soy sauce" } }, + { "id": "lime", "name": { "de": "Limette", "en": "Lime" } }, + { "id": "garlic", "name": { "de": "Knoblauch", "en": "Garlic" } }, + { "id": "cocoa_powder", "name": { "de": "Kakaopulver", "en": "Cocoa powder" } }, + { "id": "honey", "name": { "de": "Honig", "en": "Honey" } }, + { "id": "dates", "name": { "de": "Datteln", "en": "Dates" } }, + { "id": "ginger", "name": { "de": "Ingwer", "en": "Ginger" } }, + { "id": "sesame_oil", "name": { "de": "Sesamöl", "en": "Sesame oil" } }, + { "id": "lemon", "name": { "de": "Zitrone", "en": "Lemon" } }, + { "id": "oregano", "name": { "de": "Oregano", "en": "Oregano" } }, + { "id": "cumin", "name": { "de": "Kreuzkümmel", "en": "Cumin" } }, + { "id": "paprika_powder", "name": { "de": "Paprikapulver", "en": "Paprika powder" } } +] diff --git a/client/src/data/foods.json b/client/src/data/foods.json new file mode 100644 index 0000000..6d750f9 --- /dev/null +++ b/client/src/data/foods.json @@ -0,0 +1,979 @@ +[ + { + "id": "protein", + "cls": "protein", + "ic": "🥚", + "title": { + "de": "Protein", + "en": "Protein" + }, + "thumb": { + "de": "Mindestens 15–20 g Protein pro 100 g, wenig Zucker & wenig Fett, kurze Zutatenliste.", + "en": "At least 15–20 g protein per 100 g, low sugar & low fat, short ingredient list." + }, + "items": [ + { + "id": "edamame", + "name": { + "de": "Edamame", + "en": "Edamame" + }, + "v": { + "de": "125 kcal · 14 g", + "en": "125 kcal · 14 g" + } + }, + { + "id": "egg", + "name": { + "de": "Ei (Größe M)", + "en": "Egg (size M)" + }, + "v": { + "de": "137 kcal · 13 g", + "en": "137 kcal · 13 g" + } + }, + { + "id": "pea_protein_strips", + "name": { + "de": "Erbsenproteinschnetzel", + "en": "Pea-protein strips" + }, + "v": { + "de": "367 kcal · 61 g", + "en": "367 kcal · 61 g" + } + }, + { + "id": "feta", + "name": { + "de": "Feta (leicht)", + "en": "Feta (light)" + }, + "v": { + "de": "161 kcal · 19 g", + "en": "161 kcal · 19 g" + }, + "f": "caution" + }, + { + "id": "chicken_breast", + "name": { + "de": "Hühnerbrust", + "en": "Chicken breast" + }, + "v": { + "de": "111 kcal · 24 g", + "en": "111 kcal · 24 g" + }, + "f": "pick" + }, + { + "id": "cottage_cheese", + "name": { + "de": "Hüttenkäse", + "en": "Cottage cheese" + }, + "v": { + "de": "87 kcal · 12,3 g", + "en": "87 kcal · 12.3 g" + } + }, + { + "id": "like_chicken", + "name": { + "de": "„Like Chicken\" (pflanzlich)", + "en": "\"Like Chicken\" (plant-based)" + }, + "v": { + "de": "≈140 kcal · 18 g", + "en": "≈140 kcal · 18 g" + }, + "approx": true + }, + { + "id": "low_fat_quark", + "name": { + "de": "Magerquark", + "en": "Low-fat quark" + }, + "v": { + "de": "67 kcal · 12 g", + "en": "67 kcal · 12 g" + } + }, + { + "id": "beef_fillet", + "name": { + "de": "Rinderfilet / Tatar", + "en": "Beef fillet / tartare" + }, + "v": { + "de": "112 kcal · 22 g", + "en": "112 kcal · 22 g" + }, + "f": "pick" + }, + { + "id": "silken_tofu", + "name": { + "de": "Seidentofu", + "en": "Silken tofu" + }, + "v": { + "de": "≈55 kcal · 5 g", + "en": "≈55 kcal · 5 g" + }, + "approx": true + }, + { + "id": "skyr", + "name": { + "de": "Skyr", + "en": "Skyr" + }, + "v": { + "de": "54 kcal · 10,3 g", + "en": "54 kcal · 10.3 g" + }, + "f": "pick" + }, + { + "id": "tempeh", + "name": { + "de": "Tempeh", + "en": "Tempeh" + }, + "v": { + "de": "184 kcal · 19 g", + "en": "184 kcal · 19 g" + } + }, + { + "id": "tofu", + "name": { + "de": "Tofu", + "en": "Tofu" + }, + "v": { + "de": "129 kcal · 13 g", + "en": "129 kcal · 13 g" + } + } + ], + "sub": { + "title": { + "de": "Milch & Joghurt", + "en": "Milk & yoghurt" + }, + "items": [ + { + "id": "greek_yogurt_10", + "name": { + "de": "Griech. Joghurt 10 %", + "en": "Greek yoghurt 10%" + }, + "v": { + "de": "≈115 kcal · 5 g", + "en": "≈115 kcal · 5 g" + }, + "approx": true + }, + { + "id": "natural_yogurt", + "name": { + "de": "Naturjoghurt", + "en": "Natural yoghurt" + }, + "v": { + "de": "≈61 kcal · 3,5 g", + "en": "≈61 kcal · 3.5 g" + }, + "approx": true + }, + { + "id": "oatly_barista", + "name": { + "de": "Oatly Barista", + "en": "Oatly Barista" + }, + "v": { + "de": "≈68 kcal · 1 g (kaum Protein)", + "en": "≈68 kcal · 1 g (barely protein)" + }, + "approx": true, + "f": "caution" + }, + { + "id": "whole_milk", + "name": { + "de": "Vollmilch 3,5 %", + "en": "Whole milk 3.5%" + }, + "v": { + "de": "≈64 kcal · 3,4 g", + "en": "≈64 kcal · 3.4 g" + }, + "approx": true, + "f": "caution" + } + ] + } + }, + { + "id": "snack", + "cls": "snack", + "ic": "🥤", + "title": { + "de": "Proteinsnacks für Steffi", + "en": "Protein snacks for Steffi" + }, + "thumb": { + "de": "Mindestens 15 g Protein pro Portion, unter 5 g Zucker, Fett unter 8 g — Zutatenliste beginnt nicht mit Zucker/Sirup/Schokolade.", + "en": "At least 15 g protein per serving, under 5 g sugar, fat under 8 g — ingredient list shouldn’t open with sugar/syrup/chocolate." + }, + "items": [ + { + "id": "three_eggs", + "name": { + "de": "3 Eier", + "en": "3 eggs" + }, + "v": { + "de": "≈205 kcal · 19 g", + "en": "≈205 kcal · 19 g" + }, + "approx": true + }, + { + "id": "cottage_berries", + "name": { + "de": "Hüttenkäse + Beeren", + "en": "Cottage cheese + berries" + }, + "v": { + "de": "≈210 kcal · 25 g", + "en": "≈210 kcal · 25 g" + }, + "approx": true + }, + { + "id": "chickpea_waffles", + "name": { + "de": "Kichererbsenwaffeln 90g", + "en": "Chickpea waffles 90g" + }, + "v": { + "de": "≈300 kcal · 17 g", + "en": "≈300 kcal · 17 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "pumpkin_seeds_30", + "name": { + "de": "Kürbiskerne 30g", + "en": "Pumpkin seeds 30g" + }, + "v": { + "de": "≈171 kcal · 9 g", + "en": "≈171 kcal · 9 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "lentil_waffles", + "name": { + "de": "Linsenwaffeln 90g", + "en": "Lentil waffles 90g" + }, + "v": { + "de": "≈300 kcal · 18 g", + "en": "≈300 kcal · 18 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "protein_bar", + "name": { + "de": "Magerer Proteinriegel", + "en": "Lean protein bar" + }, + "v": { + "de": "≈180 kcal · 20 g", + "en": "≈180 kcal · 20 g" + }, + "approx": true + }, + { + "id": "protein_rice_pudding", + "name": { + "de": "Proteinmilchreis (2x)", + "en": "Protein rice pudding (2x)" + }, + "v": { + "de": "≈340 kcal · 40 g", + "en": "≈340 kcal · 40 g" + }, + "approx": true + }, + { + "id": "protein_pudding", + "name": { + "de": "Proteinpudding", + "en": "Protein pudding" + }, + "v": { + "de": "≈90 kcal · 20 g", + "en": "≈90 kcal · 20 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "protein_shake", + "name": { + "de": "Proteinshake 40g", + "en": "Protein shake 40g" + }, + "v": { + "de": "≈150 kcal · 30 g", + "en": "≈150 kcal · 30 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "turkey_breast_pack", + "name": { + "de": "Putenbrust, 1 Packung", + "en": "Turkey breast, 1 pack" + }, + "v": { + "de": "≈130 kcal · 28 g", + "en": "≈130 kcal · 28 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "smoked_tofu_cubes", + "name": { + "de": "Räuchertofu-Würfel", + "en": "Smoked tofu cubes" + }, + "v": { + "de": "≈225 kcal · 25 g", + "en": "≈225 kcal · 25 g" + }, + "approx": true + }, + { + "id": "skyr_honey", + "name": { + "de": "Skyr/Griech. Joghurt 10% + Honig, 250g+", + "en": "Skyr/Greek yoghurt 10% + honey, 250g+" + }, + "v": { + "de": "≈180 kcal · 25 g", + "en": "≈180 kcal · 25 g" + }, + "approx": true + } + ], + "recipe": { + "title": { + "de": "Rezept: Whey-Waffeln", + "en": "Recipe: Whey waffles" + }, + "text": { + "de": "40 g Whey-Protein + 1 Ei + 40 g Haferflocken (fein gemahlen) + 60 ml Milch/Wasser verquirlen. ≈25 g Protein.", + "en": "Whisk 40 g whey protein + 1 egg + 40 g oats (finely ground) + 60 ml milk/water. ≈25 g protein." + } + } + }, + { + "id": "obst", + "cls": "obst", + "ic": "🍎", + "title": { + "de": "Obst", + "en": "Fruit" + }, + "thumb": { + "de": "Frisch oder tiefgekühlt ohne Zuckerzusatz, Saison bevorzugen.", + "en": "Fresh or frozen without added sugar, prefer what’s in season." + }, + "items": [ + { + "id": "apple", + "name": { + "de": "Apfel", + "en": "Apple" + }, + "v": { + "de": "Vit. C, Kalium, Kupfer", + "en": "Vit. C, potassium, copper" + } + }, + { + "id": "banana", + "name": { + "de": "Banane", + "en": "Banana" + }, + "v": { + "de": "Kalium, B6, C, Magnesium", + "en": "Potassium, B6, C, magnesium" + }, + "f": "caution" + }, + { + "id": "blueberry", + "name": { + "de": "Blaubeere", + "en": "Blueberry" + }, + "v": { + "de": "Vit. K, C, Mangan", + "en": "Vit. K, C, manganese" + }, + "f": "pick" + }, + { + "id": "strawberry", + "name": { + "de": "Erdbeere", + "en": "Strawberry" + }, + "v": { + "de": "Vit. C, Mangan, Folsäure, Kalium", + "en": "Vit. C, manganese, folate, potassium" + } + }, + { + "id": "granadilla", + "name": { + "de": "Granadilla (≈ Passionsfrucht)", + "en": "Granadilla (≈ passion fruit)" + }, + "v": { + "de": "Vit. C, A, Kalium, B2", + "en": "Vit. C, A, potassium, B2" + }, + "approx": true + }, + { + "id": "pomegranate", + "name": { + "de": "Granatapfel", + "en": "Pomegranate" + }, + "v": { + "de": "Vit. C, K, Folsäure, Kalium, B6", + "en": "Vit. C, K, folate, potassium, B6" + }, + "f": "pick" + }, + { + "id": "mango", + "name": { + "de": "Mango", + "en": "Mango" + }, + "v": { + "de": "Vit. C, A, B6, Folsäure, Kupfer", + "en": "Vit. C, A, B6, folate, copper" + }, + "f": "pick" + }, + { + "id": "nectarine", + "name": { + "de": "Nektarine", + "en": "Nectarine" + }, + "v": { + "de": "Vit. C, A, Kalium, E, Niacin", + "en": "Vit. C, A, potassium, E, niacin" + } + }, + { + "id": "orange", + "name": { + "de": "Orange", + "en": "Orange" + }, + "v": { + "de": "Vit. C, Folsäure, Kalium, Calcium", + "en": "Vit. C, folate, potassium, calcium" + } + }, + { + "id": "passion_fruit", + "name": { + "de": "Passionsfrucht", + "en": "Passion fruit" + }, + "v": { + "de": "Vit. C, A, Kalium, Eisen", + "en": "Vit. C, A, potassium, iron" + }, + "approx": true + }, + { + "id": "plum", + "name": { + "de": "Pflaume", + "en": "Plum" + }, + "v": { + "de": "Vit. C, K, Kalium, A, Kupfer", + "en": "Vit. C, K, potassium, A, copper" + } + }, + { + "id": "grapes", + "name": { + "de": "Trauben", + "en": "Grapes" + }, + "v": { + "de": "Vit. K, C, Kalium, Kupfer, Mangan", + "en": "Vit. K, C, potassium, copper, manganese" + }, + "f": "caution" + } + ] + }, + { + "id": "gemuese", + "cls": "gemuese", + "ic": "🥦", + "title": { + "de": "Gemüse", + "en": "Vegetables" + }, + "thumbLabel": { + "de": "Tipp", + "en": "Tip" + }, + "thumb": { + "de": "TK-Gemüse ist völlig okay, wenn’s schnell gehen muss — genauso nährstoffreich wie frisch, einfach ohne Salz-/Fettzusatz wählen.", + "en": "Frozen veg is perfectly fine when time is short — just as nutrient-rich as fresh, just pick it without added salt/fat." + }, + "items": [ + { + "id": "broccoli", + "name": { + "de": "Brokkoli", + "en": "Broccoli" + }, + "v": { + "de": "Vit. C, K, Folsäure, Eisen", + "en": "Vit. C, K, folate, iron" + } + }, + { + "id": "mushrooms", + "name": { + "de": "Champignons", + "en": "Mushrooms" + }, + "v": { + "de": "Kalium, Selen, Kupfer, B2", + "en": "Potassium, selenium, copper, B2" + } + }, + { + "id": "iceberg_lettuce", + "name": { + "de": "Eisbergsalat", + "en": "Iceberg lettuce" + }, + "v": { + "de": "Vit. K, A, Folsäure, Kalium — nährstoffarm", + "en": "Vit. K, A, folate, potassium — low nutrient density" + }, + "f": "caution" + }, + { + "id": "lambs_lettuce", + "name": { + "de": "Feldsalat", + "en": "Lamb’s lettuce" + }, + "v": { + "de": "Vit. A, C, Folsäure, Eisen", + "en": "Vit. A, C, folate, iron" + }, + "f": "pick" + }, + { + "id": "cucumber", + "name": { + "de": "Gurke", + "en": "Cucumber" + }, + "v": { + "de": "Vit. K, Kalium, Mangan", + "en": "Vit. K, potassium, manganese" + } + }, + { + "id": "carrot", + "name": { + "de": "Karotte", + "en": "Carrot" + }, + "v": { + "de": "Vit. A, K, Kalium, B6", + "en": "Vit. A, K, potassium, B6" + }, + "f": "pick" + }, + { + "id": "bell_pepper", + "name": { + "de": "Paprika", + "en": "Bell pepper" + }, + "v": { + "de": "Vit. C, A, B6, E", + "en": "Vit. C, A, B6, E" + } + }, + { + "id": "spinach", + "name": { + "de": "Spinat", + "en": "Spinach" + }, + "v": { + "de": "Vit. K, A, Folsäure, Eisen", + "en": "Vit. K, A, folate, iron" + }, + "f": "pick" + }, + { + "id": "tomato", + "name": { + "de": "Tomate", + "en": "Tomato" + }, + "v": { + "de": "Vit. C, K, Kalium, Folsäure", + "en": "Vit. C, K, potassium, folate" + } + }, + { + "id": "zucchini", + "name": { + "de": "Zucchini", + "en": "Zucchini" + }, + "v": { + "de": "Vit. C, Kalium, B6, Mangan", + "en": "Vit. C, potassium, B6, manganese" + } + } + ], + "note": { + "de": "Hinweis: Süßkartoffel, Kochbanane und Erbsen zählen hier als Kohlenhydratquelle, Avocado als Fettquelle — siehe dort.", + "en": "Note: sweet potato, plantain and peas count here as a carb source, avocado as a fat source — see those cards." + } + }, + { + "id": "fett", + "cls": "fett", + "ic": "🥑", + "title": { + "de": "Fett", + "en": "Fat" + }, + "thumb": { + "de": "Native/kaltgepresste Öle, ungesalzene & ungeröstete Nüsse/Kerne, Zutatenliste = nur die Zutat selbst.", + "en": "Native/cold-pressed oils, unsalted & unroasted nuts/seeds, ingredient list = just the ingredient itself." + }, + "items": [ + { + "id": "avocado", + "name": { + "de": "Avocado", + "en": "Avocado" + }, + "v": { + "de": "217 kcal · 12,5 g", + "en": "217 kcal · 12.5 g" + } + }, + { + "id": "chia_seeds", + "name": { + "de": "Chiasamen", + "en": "Chia seeds" + }, + "v": { + "de": "486 kcal · 31 g", + "en": "486 kcal · 31 g" + }, + "f": "pick" + }, + { + "id": "pumpkin_seeds", + "name": { + "de": "Kürbiskerne", + "en": "Pumpkin seeds" + }, + "v": { + "de": "570 kcal · 44 g", + "en": "570 kcal · 44 g" + } + }, + { + "id": "linseed_oil", + "name": { + "de": "Leinöl", + "en": "Linseed oil" + }, + "v": { + "de": "837 kcal · 93 g", + "en": "837 kcal · 93 g" + }, + "f": "pick" + }, + { + "id": "almonds", + "name": { + "de": "Mandeln", + "en": "Almonds" + }, + "v": { + "de": "612 kcal · 55 g", + "en": "612 kcal · 55 g" + } + }, + { + "id": "olive_oil", + "name": { + "de": "Olivenöl", + "en": "Olive oil" + }, + "v": { + "de": "857 kcal · 91,5 g", + "en": "857 kcal · 91.5 g" + } + }, + { + "id": "walnuts", + "name": { + "de": "Walnüsse", + "en": "Walnuts" + }, + "v": { + "de": "674 kcal · 62,5 g", + "en": "674 kcal · 62.5 g" + }, + "f": "pick" + } + ] + }, + { + "id": "kh", + "cls": "kh", + "ic": "🌾", + "title": { + "de": "Kohlenhydrate", + "en": "Carbohydrates" + }, + "thumb": { + "de": "Vollkorn als erste Zutat, wenig zugesetzter Zucker. Faustregel 10:1 — pro 10 g Kohlenhydrate mind. 1 g Ballaststoffe (z. B. 30 g KH → mind. 3 g Ballaststoffe).", + "en": "Whole grain as the first ingredient, low added sugar. 10:1 rule of thumb — at least 1 g fibre per 10 g carbs (e.g. 30 g carbs → at least 3 g fibre)." + }, + "items": [ + { + "id": "brown_lentils", + "name": { + "de": "Berglinsen", + "en": "Brown lentils" + }, + "v": { + "de": "319 kcal · ≈46 g", + "en": "319 kcal · ≈46 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "buckwheat", + "name": { + "de": "Buchweizen", + "en": "Buckwheat" + }, + "v": { + "de": "343 kcal · 71,5 g", + "en": "343 kcal · 71.5 g" + } + }, + { + "id": "puffed_spelt", + "name": { + "de": "Dinkel gepufft", + "en": "Puffed spelt" + }, + "v": { + "de": "≈338 kcal · 70 g", + "en": "≈338 kcal · 70 g" + }, + "approx": true + }, + { + "id": "spelt_pasta", + "name": { + "de": "Dinkel-Vollkorn-Nudeln", + "en": "Whole-grain spelt pasta" + }, + "v": { + "de": "≈338 kcal · 70 g", + "en": "≈338 kcal · 70 g" + }, + "approx": true + }, + { + "id": "peas", + "name": { + "de": "Erbsen", + "en": "Peas" + }, + "v": { + "de": "93 kcal · 10,4 g", + "en": "93 kcal · 10.4 g" + } + }, + { + "id": "oats", + "name": { + "de": "Haferflocken", + "en": "Oats" + }, + "v": { + "de": "372 kcal · 66,3 g", + "en": "372 kcal · 66.3 g" + } + }, + { + "id": "millet", + "name": { + "de": "Hirse", + "en": "Millet" + }, + "v": { + "de": "363 kcal · 69 g", + "en": "363 kcal · 69 g" + }, + "f": "caution" + }, + { + "id": "chickpeas", + "name": { + "de": "Kichererbsen", + "en": "Chickpeas" + }, + "v": { + "de": "108 kcal · 14,2 g", + "en": "108 kcal · 14.2 g" + } + }, + { + "id": "chickpea_pasta", + "name": { + "de": "Kichererbsennudeln / -couscous", + "en": "Chickpea pasta / couscous" + }, + "v": { + "de": "≈335 kcal · 50 g", + "en": "≈335 kcal · 50 g" + }, + "approx": true + }, + { + "id": "plantain", + "name": { + "de": "Kochbanane", + "en": "Plantain" + }, + "v": { + "de": "≈122 kcal · 32 g", + "en": "≈122 kcal · 32 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "lentil_pasta", + "name": { + "de": "Linsennudeln", + "en": "Lentil pasta" + }, + "v": { + "de": "≈335 kcal · 52 g", + "en": "≈335 kcal · 52 g" + }, + "approx": true + }, + { + "id": "quinoa", + "name": { + "de": "Quinoa", + "en": "Quinoa" + }, + "v": { + "de": "368 kcal · 64,2 g", + "en": "368 kcal · 64.2 g" + } + }, + { + "id": "red_lentils", + "name": { + "de": "Rote Linsen", + "en": "Red lentils" + }, + "v": { + "de": "326 kcal · ≈52 g", + "en": "326 kcal · ≈52 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "sweet_potato", + "name": { + "de": "Süßkartoffel", + "en": "Sweet potato" + }, + "v": { + "de": "86 kcal · 20,1 g", + "en": "86 kcal · 20.1 g" + }, + "f": "caution" + }, + { + "id": "french_lentils", + "name": { + "de": "Tellerlinsen", + "en": "French lentils" + }, + "v": { + "de": "234 kcal · ≈34 g", + "en": "234 kcal · ≈34 g" + }, + "approx": true, + "f": "pick" + } + ] + } +] \ No newline at end of file diff --git a/client/src/data/recipes.json b/client/src/data/recipes.json new file mode 100644 index 0000000..f22842b --- /dev/null +++ b/client/src/data/recipes.json @@ -0,0 +1,160 @@ +[ + { + "id": "bf_skyr_oats_blueberry", + "type": "breakfast", + "name": { + "de": "Skyr + Haferflocken + Blaubeeren", + "en": "Skyr + oats + blueberries" + }, + "uses": ["skyr", "oats", "blueberry"] + }, + { + "id": "bf_eggs_tomato", + "type": "breakfast", + "name": { "de": "2 Eier + Tomate", "en": "2 eggs + tomato" }, + "uses": ["egg", "tomato"] + }, + { + "id": "bf_quark_pomegranate", + "type": "breakfast", + "name": { + "de": "Magerquark + Granatapfel", + "en": "Low-fat quark + pomegranate" + }, + "uses": ["low_fat_quark", "pomegranate"] + }, + { + "id": "sn_skyr_blueberry", + "type": "snack", + "name": { "de": "Skyr + Blaubeeren", "en": "Skyr + blueberries" }, + "uses": ["skyr", "blueberry"] + }, + { + "id": "sn_cottage_cucumber", + "type": "snack", + "name": { + "de": "Hüttenkäse + Gurke", + "en": "Cottage cheese + cucumber" + }, + "uses": ["cottage_cheese", "cucumber"] + }, + { + "id": "sn_quark_pomegranate", + "type": "snack", + "name": { + "de": "Magerquark + Granatapfel", + "en": "Low-fat quark + pomegranate" + }, + "uses": ["low_fat_quark", "pomegranate"] + }, + { + "id": "sn_protein_pudding", + "type": "snack", + "name": { "de": "Proteinpudding", "en": "Protein pudding" }, + "uses": ["protein_pudding"] + }, + { + "id": "full_chicken_cooked", + "type": "full", + "cooked": true, + "name": { + "de": "Hühnerbrust warm + Gemüse + Leinöl + Rote Linsen", + "en": "Warm chicken breast + veg + linseed oil + red lentils" + }, + "uses": ["chicken_breast", "carrot", "spinach", "linseed_oil", "red_lentils"] + }, + { + "id": "full_cold_platter", + "type": "full", + "cooked": false, + "name": { + "de": "Kalte Brotzeit: Feta + Feldsalat + Walnüsse + Nudelsalat", + "en": "Cold platter: feta + lamb's lettuce + walnuts + pasta salad" + }, + "uses": ["feta", "lambs_lettuce", "walnuts", "spelt_pasta"] + }, + { + "id": "full_bowl_nocook", + "type": "full", + "cooked": false, + "name": { + "de": "Bowl: Räuchertofu + Spinat + Avocado + Tellerlinsen (Dose)", + "en": "Bowl: smoked tofu + spinach + avocado + tinned French lentils" + }, + "uses": ["smoked_tofu_cubes", "spinach", "avocado", "french_lentils"] + }, + { + "id": "full_leftovers_cold", + "type": "full", + "cooked": false, + "name": { + "de": "Resteteller: Hühnerbrust + Karotte + Walnüsse + Rote Linsen (kalt)", + "en": "Leftovers plate: chicken + carrot + walnuts + red lentils (cold)" + }, + "uses": ["chicken_breast", "carrot", "walnuts", "red_lentils"] + }, + { + "id": "rec_protein_shake", + "type": "recipe", + "name": { "de": "Proteinshake — schnell", "en": "Protein shake — quick" }, + "uses": ["protein_shake"], + "text": { + "de": "40 g Proteinpulver + 250–300 ml Wasser oder Milch shaken. ≈30 g Protein.", + "en": "Shake 40 g protein powder with 250–300 ml water or milk. ≈30 g protein." + } + }, + { + "id": "rec_tempeh_pan", + "type": "recipe", + "name": { "de": "Tempeh-Pfanne", "en": "Pan-fried tempeh" }, + "uses": ["tempeh", "soy_sauce", "lime", "garlic"], + "text": { + "de": "Tempeh würfeln, 10 Min. in Sojasauce + Limette + Knoblauch marinieren, knusprig braten (5–7 Min.).", + "en": "Cube tempeh, marinate 10 min in soy sauce + lime + garlic, pan-fry crisp (5–7 min)." + } + }, + { + "id": "rec_silken_tofu_pudding", + "type": "recipe", + "name": { + "de": "Schoko-Seidentofu-Pudding", + "en": "Chocolate silken-tofu pudding" + }, + "uses": ["silken_tofu", "cocoa_powder", "honey"], + "text": { + "de": "150 g Seidentofu + 1 EL Kakaopulver + 1 TL Honig pürieren, 30 Min. kalt stellen.", + "en": "Blend 150 g silken tofu + 1 tbsp cocoa powder + 1 tsp honey, chill 30 min." + } + }, + { + "id": "rec_veggie_stirfry_base", + "type": "recipe", + "name": { + "de": "Grundrezept: Gemüsepfanne", + "en": "Base recipe: vegetable stir-fry" + }, + "uses": ["carrot", "bell_pepper", "zucchini", "olive_oil"], + "text": { + "de": "Gemüse in mundgerechte Stücke schneiden. In heißem Öl 5–8 Min. anbraten. Protein dazugeben, kurz mitbraten, würzen, fertig.", + "en": "Cut vegetables into bite-size pieces. Pan-fry in hot oil 5–8 min. Add protein, fry briefly, season, done." + }, + "variations": [ + { + "de": "Asiatisch: Ingwer + Sojasauce + Sesamöl", + "en": "Asian: ginger + soy sauce + sesame oil" + }, + { + "de": "Mediterran: Olivenöl + Zitrone + Oregano", + "en": "Mediterranean: olive oil + lemon + oregano" + }, + { + "de": "Mexikanisch: Kreuzkümmel + Paprikapulver + Limette", + "en": "Mexican: cumin + paprika powder + lime" + }, + { + "de": "Protein nach Wahl: Hühnerbrust, Tempeh, Räuchertofu oder Kichererbsen", + "en": "Protein of choice: chicken breast, tempeh, smoked tofu, or chickpeas" + } + ] + } +] diff --git a/client/src/data/steps.json b/client/src/data/steps.json new file mode 100644 index 0000000..4342d7a --- /dev/null +++ b/client/src/data/steps.json @@ -0,0 +1,66 @@ +[ + { + "n": 1, + "cls": "g", + "title": { + "de": "Protein", + "en": "Protein" + }, + "qty": { + "de": "30 g pro Mahlzeit", + "en": "30 g per meal" + }, + "text": { + "de": "Bspw. 125 g Rinderfilet, 300 g Skyr, 40 g Proteinpulver, 50 g Erbsenproteingeschnetzeltes oder 5 Eier. 150 g am Tag insgesamt.", + "en": "E.g. 125 g beef fillet, 300 g Skyr, 40 g protein powder, 50 g pea-protein strips, or 5 eggs. 150 g total per day." + } + }, + { + "n": 2, + "cls": "t", + "title": { + "de": "Obst ODER Gemüse", + "en": "Fruit OR Vegetables" + }, + "qty": { + "de": "eine Portion", + "en": "one portion" + }, + "text": { + "de": "Obst = 100 g oder eine geballte Faust. Gemüse = 200 g oder 2 Handvoll.", + "en": "Fruit = 100 g or one clenched fist. Vegetables = 200 g or 2 handfuls." + } + }, + { + "n": 3, + "cls": "o", + "title": { + "de": "Fett", + "en": "Fat" + }, + "qty": { + "de": "", + "en": "" + }, + "text": { + "de": "15 g Samen/Kerne/Nüsse (≈20 Stück, 1 EL) · ¼ Avocado · 1 EL Öl (15 g) — Nussöl außer Erdnuss, Olivenöl, Leinsamenöl.", + "en": "15 g seeds/nuts (≈20 pieces, 1 tbsp) · ¼ avocado · 1 tbsp oil (15 g) — nut oil except peanut, olive oil, linseed oil." + } + }, + { + "n": 4, + "cls": "s", + "title": { + "de": "Kohlenhydrate", + "en": "Carbohydrates" + }, + "qty": { + "de": "", + "en": "" + }, + "text": { + "de": "75 g Reis/Nudeln/Brot (trocken) · ca. 250 g rohe Kartoffeln (≈3 mittelgroße) · 50 g Haferflocken (≈5 gehäufte EL).", + "en": "75 g rice/pasta/bread (dry) · approx. 250 g raw potatoes (≈3 medium) · 50 g oats (≈5 heaped tbsp)." + } + } +] \ No newline at end of file diff --git a/client/src/i18n/ui.ts b/client/src/i18n/ui.ts new file mode 100644 index 0000000..a377d7f --- /dev/null +++ b/client/src/i18n/ui.ts @@ -0,0 +1,148 @@ +import type { Language, LocalizedString } from "@/types/domain" + +/** + * Resolve a UI chrome key object (or any LocalizedString) for the active language. + * Mirrors the prototype `t(key)` helper when called as `t(ui.nav.home, lang)`. + */ +export function t(value: LocalizedString, lang: Language): string { + return value[lang] +} + +/** + * Resolve a data LocalizedString (`name`, `text`, …) — prototype `tr(obj)`. + */ +export function tr(value: LocalizedString, lang: Language): string { + return value[lang] +} + +export const ui = { + appName: { de: "Pocket Pascal", en: "Pocket Pascal" }, + nav: { + home: { de: "Start", en: "Home" }, + pantry: { de: "Vorrat", en: "Pantry" }, + knowledge: { de: "Teller", en: "Plate" }, + settings: { de: "Einstellungen", en: "Settings" }, + }, + home: { + title: { de: "Was esse ich jetzt?", en: "What do I eat now?" }, + editLayout: { de: "Layout", en: "Layout" }, + doneEdit: { de: "Fertig", en: "Done" }, + moveUp: { de: "Nach oben", en: "Move up" }, + moveDown: { de: "Nach unten", en: "Move down" }, + }, + suggestion: { + morning: { de: "Morgen · Protein + Obst", en: "Morning · Protein + fruit" }, + midday: { de: "Mittag · Ganzer Teller", en: "Midday · Full plate" }, + afternoon: { de: "Nachmittag · Snack-Zeit", en: "Afternoon · Snack time" }, + evening: { de: "Abend · Ganzer Teller", en: "Evening · Full plate" }, + timeSuffix: { de: "Uhr", en: "" }, + pinned: { de: "Fixiert", en: "Pinned" }, + pin: { de: "Fixieren", en: "Pin" }, + unpin: { de: "Lösen", en: "Unpin" }, + reroll: { de: "Was anderes", en: "Something else" }, + toBuilder: { de: "In den Builder übernehmen", en: "Send to builder" }, + fallbackName: { de: "Dein Teller jetzt", en: "Your plate now" }, + }, + knowledgeHub: { + plateTitle: { de: "Mein Teller", en: "My Plate" }, + plateBody: { + de: "⅓ Protein · ⅓ Gemüse/Obst · ⅓ Fett + Kohlenhydrate", + en: "⅓ protein · ⅓ veg/fruit · ⅓ fat + carbs", + }, + pantryTitle: { de: "Vorratskammer", en: "Pantry" }, + pantryBody: { + de: "Kategorien, Suche, Pick- & Caution-Markierungen", + en: "Categories, search, pick & caution markers", + }, + weekplanTitle: { de: "Wochenplan", en: "Weekly plan" }, + weekplanBody: { + de: "Frühstück, Snacks, volle Teller & Rezepte", + en: "Breakfast, snacks, full plates & recipes", + }, + }, + weekplan: { + title: { de: "Wochenplan", en: "Weekly plan" }, + intro: { + de: "Kuratierte Kombinationen aus dem Baukasten — gruppiert nach Mahlzeitentyp.", + en: "Curated building-block combos — grouped by meal type.", + }, + breakfast: { de: "Frühstück", en: "Breakfast" }, + snack: { de: "Snack", en: "Snack" }, + full: { de: "Ganzer Teller", en: "Full plate" }, + recipe: { de: "Rezepte", en: "Recipes" }, + cooked: { de: "Warm / gekocht", en: "Warm / cooked" }, + cold: { de: "Kalt / ohne Kochen", en: "Cold / no cook" }, + uses: { de: "Zutaten", en: "Ingredients" }, + variations: { de: "Variationen", en: "Variations" }, + }, + pantry: { + title: { de: "Vorratskammer", en: "Pantry" }, + search: { de: "Lebensmittel suchen…", en: "Search foods…" }, + pick: { de: "empfohlen", en: "recommended" }, + caution: { de: "bewusst genießen", en: "enjoy mindfully" }, + noResults: { de: "Keine Treffer.", en: "No results." }, + tip: { de: "Tipp", en: "Tip" }, + }, + plate: { + title: { de: "Mein Teller", en: "My Plate" }, + subtitle: { + de: "Baukasten-Reihenfolge: zuerst Protein, dann Obst oder Gemüse, danach Fett und Kohlenhydrate.", + en: "Building-block order: protein first, then fruit or vegetables, then fat and carbohydrates.", + }, + mealToggle: { de: "Mahlzeitentyp", en: "Meal type" }, + segFull: { de: "Ganze Mahlzeit", en: "Full meal" }, + segHalf: { de: "Halbe Mahlzeit", en: "Half meal" }, + plateAria: { + de: "Tellerverteilung nach Baukasten", + en: "Plate breakdown by building blocks", + }, + rulesTitle: { de: "Grundregeln", en: "Ground rules" }, + rule1: { + de: "Jede Mahlzeit beruht auf Protein — egal ob süß, herzhaft, Snack oder Hauptmahlzeit.", + en: "Every meal is built on protein — sweet, savoury, snack, or main.", + }, + rule2: { + de: "Jede Mahlzeit enthält eine Portion Gemüse oder Obst.", + en: "Every meal includes a portion of vegetables or fruit.", + }, + rule3: { + de: "4 Mahlzeiten am Tag, ca. alle 3–4 Std.", + en: "4 meals a day, roughly every 3–4 hours.", + }, + rule4: { + de: "2 L Wasser täglich.", + en: "2 L of water daily.", + }, + }, + builderSlot: { + title: { de: "Teller-Builder", en: "Plate builder" }, + body: { + de: "Chips antippen, Kombination sofort gegen die Baukasten-Regeln prüfen.", + en: "Tap chips and check the combo against the building-block rules instantly.", + }, + }, + stubs: { + pantryTitle: { de: "Vorratskammer", en: "Pantry" }, + pantryBody: { + de: "Phase-1-Platzhalter — Suche & Kategorien folgen.", + en: "Phase 1 stub — search & categories coming next.", + }, + knowledgeTitle: { de: "Knowledge Base", en: "Knowledge Base" }, + knowledgeBody: { + de: "Phase-1-Platzhalter — Teller-Regel & Daumenregeln folgen.", + en: "Phase 1 stub — plate rule & thumb rules coming next.", + }, + builderTitle: { de: "Teller-Builder", en: "Plate builder" }, + builderBody: { + de: "Phase-1-Platzhalter — Chip-Auswahl & Bewertung folgen.", + en: "Phase 1 stub — chip selection & scoring coming next.", + }, + builderHandoff: { + de: "Übernommen aus dem Vorschlag:", + en: "Handed off from suggestion:", + }, + settingsTitle: { de: "Einstellungen", en: "Settings" }, + language: { de: "Sprache", en: "Language" }, + comingSoon: { de: "Demnächst", en: "Coming soon" }, + }, +} as const diff --git a/client/src/index.css b/client/src/index.css index fb3c7e9..a553b93 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,13 +1,14 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; -@import "@fontsource-variable/geist"; +@import "./styles/theme.css"; +@import "./styles/glass.css"; @custom-variant dark (&:is(.dark *)); @theme inline { - --font-heading: var(--font-sans); - --font-sans: 'Geist Variable', sans-serif; + --font-heading: "Unbounded", ui-sans-serif, sans-serif; + --font-sans: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -121,10 +122,15 @@ * { @apply border-border outline-ring/50; } - body { - @apply bg-background text-foreground; - } html { @apply font-sans; } + body { + margin: 0; + min-height: 100svh; + color: var(--ink); + background: var(--page-gradient); + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + -webkit-tap-highlight-color: transparent; + } } \ No newline at end of file diff --git a/client/src/lib/catalog.ts b/client/src/lib/catalog.ts new file mode 100644 index 0000000..d280462 --- /dev/null +++ b/client/src/lib/catalog.ts @@ -0,0 +1,139 @@ +import type { + CategoryId, + CatalogItem, + ExtraItem, + FoodCategory, + FoodItem, + LocalizedString, + PlanRecipeType, + Recipe, + TagType, + TimeSlot, +} from "@/types/domain" +import extrasRaw from "@/data/extras.json" +import foodsRaw from "@/data/foods.json" +import recipesRaw from "@/data/recipes.json" +import stepsRaw from "@/data/steps.json" + +interface RawFoodItem { + id: string + name: LocalizedString + v: LocalizedString + f?: TagType + approx?: boolean +} + +interface RawFoodCategory { + id: CategoryId + cls: string + title: LocalizedString + thumb: LocalizedString + items: RawFoodItem[] +} + +export interface PlateStep { + n: number + cls: string + title: LocalizedString + qty: LocalizedString + text: LocalizedString +} + +function mapItem(cat: CategoryId, item: RawFoodItem): FoodItem { + return { + id: item.id, + cat, + name: item.name, + valueInfo: item.v, + tag: item.f, + isApprox: item.approx, + } +} + +export const foodCategories: FoodCategory[] = (foodsRaw as RawFoodCategory[]).map( + (category) => ({ + id: category.id, + cls: category.cls, + title: category.title, + thumb: category.thumb, + items: category.items.map((item) => mapItem(category.id, item)), + }), +) + +export const foods: FoodItem[] = foodCategories.flatMap((category) => category.items) + +export const foodById: Record = Object.fromEntries( + foods.map((item) => [item.id, item]), +) + +export const extras: ExtraItem[] = extrasRaw as ExtraItem[] + +export const extraById: Record = Object.fromEntries( + extras.map((item) => [item.id, item]), +) + +export const recipes: Recipe[] = recipesRaw as Recipe[] + +export const plateSteps: PlateStep[] = stepsRaw as PlateStep[] + +export const PLAN_TYPE_ORDER: PlanRecipeType[] = [ + "breakfast", + "snack", + "full", + "recipe", +] + +export function foodsInCategory(cat: CategoryId): FoodItem[] { + return foodCategories.find((category) => category.id === cat)?.items ?? [] +} + +export function resolveCatalogId(id: string): CatalogItem | null { + const food = foodById[id] + if (food) { + return { + id: food.id, + name: food.name, + source: "cat", + cat: food.cat, + valueInfo: food.valueInfo, + tag: food.tag, + } + } + const extra = extraById[id] + if (extra) { + return { + id: extra.id, + name: extra.name, + source: "extra", + } + } + return null +} + +export function resolveRecipeUses(recipe: Recipe): CatalogItem[] { + return recipe.uses + .map((id) => resolveCatalogId(id)) + .filter((item): item is CatalogItem => Boolean(item)) +} + +export function recipesByType(type: PlanRecipeType): Recipe[] { + return recipes.filter((recipe) => recipe.type === type) +} + +/** Map plan recipe types onto suggestion time slots. */ +export function timeSlotsForRecipe(recipe: Recipe): TimeSlot[] { + switch (recipe.type) { + case "breakfast": + return ["morning"] + case "snack": + return ["afternoon"] + case "full": + return ["midday", "evening"] + case "recipe": + return ["morning", "afternoon", "midday", "evening"] + } +} + +export function mealTypeForRecipe(recipe: Recipe): "full" | "half" { + return recipe.type === "full" ? "full" : "half" +} diff --git a/client/src/lib/cx.ts b/client/src/lib/cx.ts new file mode 100644 index 0000000..3d230a6 --- /dev/null +++ b/client/src/lib/cx.ts @@ -0,0 +1,4 @@ +/** Tiny className joiner — avoids pulling in clsx/tailwind-merge as a dependency. */ +export function cx(...parts: Array): string { + return parts.filter(Boolean).join(" "); +} diff --git a/client/src/lib/language.tsx b/client/src/lib/language.tsx new file mode 100644 index 0000000..5f3df92 --- /dev/null +++ b/client/src/lib/language.tsx @@ -0,0 +1,65 @@ +import { + createContext, + createElement, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react" + +import type { Language } from "@/types/domain" + +const STORAGE_KEY = "pp.lang" + +interface LanguageContextValue { + lang: Language + setLang: (lang: Language) => void + toggleLang: () => void +} + +const LanguageContext = createContext(null) + +function readStoredLanguage(): Language { + try { + const value = localStorage.getItem(STORAGE_KEY) + if (value === "de" || value === "en") return value + } catch { + // ignore + } + return "de" +} + +export function LanguageProvider({ children }: { children: ReactNode }) { + const [lang, setLangState] = useState(() => + typeof window === "undefined" ? "de" : readStoredLanguage(), + ) + + useEffect(() => { + try { + localStorage.setItem(STORAGE_KEY, lang) + } catch { + // ignore + } + document.documentElement.lang = lang + }, [lang]) + + const value = useMemo( + () => ({ + lang, + setLang: setLangState, + toggleLang: () => setLangState((current) => (current === "de" ? "en" : "de")), + }), + [lang], + ) + + return createElement(LanguageContext.Provider, { value }, children) +} + +export function useLanguage(): LanguageContextValue { + const ctx = useContext(LanguageContext) + if (!ctx) { + throw new Error("useLanguage must be used within LanguageProvider") + } + return ctx +} diff --git a/client/src/lib/suggestion.ts b/client/src/lib/suggestion.ts new file mode 100644 index 0000000..f71fd15 --- /dev/null +++ b/client/src/lib/suggestion.ts @@ -0,0 +1,103 @@ +import { foodById, foods, recipes, timeSlotsForRecipe } from "@/lib/catalog" +import type { CategoryId, FoodItem, Recipe, TimeSlot } from "@/types/domain" + +const SLOT_CATEGORIES: Record = { + morning: ["protein", "obst", "fett"], + midday: ["protein", "gemuese", "fett", "kh"], + afternoon: ["protein", "gemuese"], + evening: ["protein", "gemuese", "fett", "kh"], +} + +function pickRandom(items: T[], excludeIds: Set, getId: (item: T) => string): T | null { + const pool = items.filter((item) => !excludeIds.has(getId(item))) + if (pool.length === 0) return null + return pool[Math.floor(Math.random() * pool.length)] ?? null +} + +export function recipesForSlot(slot: TimeSlot): Recipe[] { + return recipes.filter( + (recipe) => + recipe.type !== "recipe" && timeSlotsForRecipe(recipe).includes(slot), + ) +} + +/** Plate foods only (skip EXTRAS) for suggestion chips / builder handoff. */ +export function resolveRecipeFoods(recipe: Recipe): FoodItem[] { + return recipe.uses + .map((id) => foodById[id]) + .filter((item): item is FoodItem => Boolean(item)) +} + +/** Prefer curated recipes; fall back to one random food per expected category. */ +export function buildSuggestion(slot: TimeSlot, avoidRecipeId?: string): { + recipe: Recipe | null + foods: FoodItem[] +} { + const candidates = recipesForSlot(slot).filter((recipe) => recipe.id !== avoidRecipeId) + const recipe = + candidates.length > 0 + ? (pickRandom(candidates, new Set(), (item) => item.id) as Recipe) + : null + + if (recipe) { + return { recipe, foods: resolveRecipeFoods(recipe) } + } + + const generated = SLOT_CATEGORIES[slot] + .map((cat) => + pickRandom( + foods.filter((item) => item.cat === cat), + new Set(), + (item) => item.id, + ), + ) + .filter((item): item is FoodItem => Boolean(item)) + + return { recipe: null, foods: generated } +} + +/** + * Replace unpinned ingredients with alternatives from the same category. + * Pinned IDs stay in place; missing categories are filled when possible. + */ +export function reshuffleSuggestion( + current: FoodItem[], + pinnedIds: Set, + slot: TimeSlot, +): FoodItem[] { + const pinned = current.filter((item) => pinnedIds.has(item.id)) + const pinnedCats = new Set(pinned.map((item) => item.cat)) + const exclude = new Set(current.map((item) => item.id)) + + const next: FoodItem[] = [...pinned] + + for (const item of current) { + if (pinnedIds.has(item.id)) continue + const replacement = pickRandom( + foods.filter((candidate) => candidate.cat === item.cat), + exclude, + (candidate) => candidate.id, + ) + if (replacement) { + exclude.add(replacement.id) + next.push(replacement) + } else { + next.push(item) + } + } + + for (const cat of SLOT_CATEGORIES[slot]) { + if (pinnedCats.has(cat) || next.some((item) => item.cat === cat)) continue + const fill = pickRandom( + foods.filter((candidate) => candidate.cat === cat), + exclude, + (candidate) => candidate.id, + ) + if (fill) { + exclude.add(fill.id) + next.push(fill) + } + } + + return next +} diff --git a/client/src/lib/timeContext.ts b/client/src/lib/timeContext.ts new file mode 100644 index 0000000..8392956 --- /dev/null +++ b/client/src/lib/timeContext.ts @@ -0,0 +1,36 @@ +import type { MealType, TimeSlot } from "@/types/domain" + +export interface TimeContext { + slot: TimeSlot + mealType: MealType + hour: number + minute: number +} + +/** Local-time meal context for the suggestion engine. */ +export function getTimeContext(date = new Date()): TimeContext { + const hour = date.getHours() + const minute = date.getMinutes() + + if (hour >= 6 && hour < 11) { + return { slot: "morning", mealType: "half", hour, minute } + } + if (hour >= 11 && hour < 15) { + return { slot: "midday", mealType: "full", hour, minute } + } + if (hour >= 15 && hour < 18) { + return { slot: "afternoon", mealType: "half", hour, minute } + } + if (hour >= 18 && hour < 22) { + return { slot: "evening", mealType: "full", hour, minute } + } + + // Late night / early morning: lean snack context + return { slot: "afternoon", mealType: "half", hour, minute } +} + +export function formatClock(hour: number, minute: number, lang: "de" | "en"): string { + const hh = String(hour).padStart(2, "0") + const mm = String(minute).padStart(2, "0") + return lang === "de" ? `${hh}:${mm}` : `${hh}:${mm}` +} diff --git a/client/src/lib/widgetOrder.ts b/client/src/lib/widgetOrder.ts new file mode 100644 index 0000000..4c44b1f --- /dev/null +++ b/client/src/lib/widgetOrder.ts @@ -0,0 +1,55 @@ +import type { HomeWidgetId, WidgetOrder } from "@/types/domain" + +const STORAGE_KEY = "pp.home.widgetOrder" + +export const DEFAULT_WIDGET_ORDER: WidgetOrder = [ + "suggestion", + "knowledge", + "builder", +] + +function isWidgetId(value: unknown): value is HomeWidgetId { + return value === "suggestion" || value === "knowledge" || value === "builder" +} + +export function readWidgetOrder(): WidgetOrder { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return DEFAULT_WIDGET_ORDER + const parsed: unknown = JSON.parse(raw) + if (!Array.isArray(parsed) || parsed.length !== 3 || !parsed.every(isWidgetId)) { + return DEFAULT_WIDGET_ORDER + } + const unique = new Set(parsed) + if (unique.size !== 3) return DEFAULT_WIDGET_ORDER + return parsed + } catch { + return DEFAULT_WIDGET_ORDER + } +} + +export function writeWidgetOrder(order: WidgetOrder): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(order)) + } catch { + // ignore + } +} + +export function moveWidget( + order: WidgetOrder, + id: HomeWidgetId, + direction: "up" | "down", +): WidgetOrder { + const index = order.indexOf(id) + if (index < 0) return order + const target = direction === "up" ? index - 1 : index + 1 + if (target < 0 || target >= order.length) return order + const next = [...order] + const current = next[index] + const swap = next[target] + if (current === undefined || swap === undefined) return order + next[index] = swap + next[target] = current + return next +} diff --git a/client/src/screens/BuilderScreen.tsx b/client/src/screens/BuilderScreen.tsx new file mode 100644 index 0000000..88d5cd6 --- /dev/null +++ b/client/src/screens/BuilderScreen.tsx @@ -0,0 +1,53 @@ +import { useLocation } from "react-router-dom" + +import { GlassCard } from "@/components/ui-pp/GlassCard" +import { t, tr, ui } from "@/i18n/ui" +import { foodById } from "@/lib/catalog" +import { useLanguage } from "@/lib/language" +import type { BuilderHandoffState } from "@/types/domain" + +export function BuilderScreen() { + const { lang } = useLanguage() + const location = useLocation() + const handoff = location.state as BuilderHandoffState | null + const ingredientIds = handoff?.ingredientIds ?? [] + + return ( +
+ +

+ {t(ui.stubs.comingSoon, lang)} +

+

+ {t(ui.stubs.builderTitle, lang)} +

+

+ {t(ui.stubs.builderBody, lang)} +

+
+ + {ingredientIds.length > 0 ? ( + +

+ {t(ui.stubs.builderHandoff, lang)} +

+
    + {ingredientIds.map((id) => { + const item = foodById[id] + if (!item) return null + return ( +
  • + {tr(item.name, lang)} +
  • + ) + })} +
+
+ ) : null} +
+ ) +} diff --git a/client/src/screens/PantryScreen.tsx b/client/src/screens/PantryScreen.tsx new file mode 100644 index 0000000..d437bf9 --- /dev/null +++ b/client/src/screens/PantryScreen.tsx @@ -0,0 +1,124 @@ +import { useMemo, useState } from "react" + +import { Accordion } from "@/components/ui-pp/Accordion" +import { t, tr, ui } from "@/i18n/ui" +import { foodCategories } from "@/lib/catalog" +import { useLanguage } from "@/lib/language" +import { cx } from "@/lib/cx" +import type { FoodItem } from "@/types/domain" + +function matchesQuery(item: FoodItem, query: string, lang: "de" | "en"): boolean { + if (!query) return true + return tr(item.name, lang).toLowerCase().includes(query) +} + +export function PantryScreen() { + const { lang } = useLanguage() + const [query, setQuery] = useState("") + const normalized = query.trim().toLowerCase() + + const sections = useMemo( + () => + foodCategories + .map((category) => { + const items = category.items.filter((item) => matchesQuery(item, normalized, lang)) + return { category, items } + }) + .filter((section) => section.items.length > 0), + [lang, normalized], + ) + + return ( +
+
+

+ {t(ui.pantry.title, lang)} +

+

+ {t(ui.pantry.pick, lang)} + {" · "} + {t(ui.pantry.caution, lang)} +

+
+ + + setQuery(event.target.value)} + placeholder={t(ui.pantry.search, lang)} + className={cx( + "type-body min-h-[48px] w-full rounded-[var(--radius-control)] px-4 text-[var(--ink)]", + "ring-1 ring-[var(--border)] outline-none placeholder:text-[var(--ink-faint)]", + "focus-visible:ring-2 focus-visible:ring-[var(--color-kh)]/40", + )} + style={{ background: "var(--glass-fill)" }} + /> + + {sections.length === 0 ? ( +

{t(ui.pantry.noResults, lang)}

+ ) : ( +
+ {sections.map(({ category, items }) => ( + + {category.id === "protein" + ? "🥚" + : category.id === "snack" + ? "🥤" + : category.id === "obst" + ? "🍎" + : category.id === "gemuese" + ? "🥦" + : category.id === "fett" + ? "🥑" + : "🌾"} + + } + > +
    + {items.map((item) => ( +
  • + + {tr(item.name, lang)} + + + {item.tag === "pick" ? ( + + ) : null} + {item.tag === "caution" ? ( + + ) : null} + {tr(item.valueInfo, lang)} + +
  • + ))} +
+

+ {t(ui.pantry.tip, lang)} +
+ {tr(category.thumb, lang)} +

+
+ ))} +
+ )} +
+ ) +} diff --git a/client/src/screens/PlateScreen.tsx b/client/src/screens/PlateScreen.tsx new file mode 100644 index 0000000..dc1afa7 --- /dev/null +++ b/client/src/screens/PlateScreen.tsx @@ -0,0 +1,283 @@ +import { useEffect, useMemo, useRef, useState } from "react" +import { ChevronRight } from "lucide-react" + +import { DiningPlate } from "@/components/plate/DiningPlate" +import { SegControl } from "@/components/ui-pp/SegControl" +import { t, tr, ui } from "@/i18n/ui" +import { plateSteps, type PlateStep } from "@/lib/catalog" +import { useLanguage } from "@/lib/language" +import { cx } from "@/lib/cx" + +type MealMode = "full" | "half" + +const CLS_COLOR: Record = { + g: "var(--green)", + t: "var(--terracotta)", + o: "var(--gold)", + s: "var(--slate)", +} + +const FULL_WEIGHT: Record = { + g: 2, + t: 2, + o: 1, + s: 1, +} + +function stepColor(cls: string): string { + return CLS_COLOR[cls] ?? "var(--ink-faint)" +} + +function StepDetailCard({ + step, + open, + highlighted, + onToggle, + lang, +}: { + step: PlateStep + open: boolean + highlighted: boolean + onToggle: () => void + lang: "de" | "en" +}) { + const color = stepColor(step.cls) + const qty = tr(step.qty, lang) + const panelId = `plate-step-panel-${step.n}` + + return ( +
+
+
+
+ + + + {open ? ( +
+

{tr(step.text, lang)}

+
+ ) : null} +
+
+ ) +} + +export function PlateScreen() { + const { lang } = useLanguage() + const [mode, setMode] = useState("full") + const [openIds, setOpenIds] = useState>(() => new Set([1])) + const [highlightId, setHighlightId] = useState(null) + const highlightTimer = useRef | null>(null) + + const visibleSteps = useMemo( + () => + mode === "full" + ? plateSteps + : plateSteps.filter((step) => step.cls === "g" || step.cls === "t"), + [mode], + ) + + const segments = useMemo( + () => + visibleSteps.map((step) => ({ + id: String(step.n), + weight: mode === "full" ? (FULL_WEIGHT[step.cls] ?? 1) : 1, + color: stepColor(step.cls), + label: tr(step.title, lang), + })), + [visibleSteps, mode, lang], + ) + + useEffect(() => { + return () => { + if (highlightTimer.current) clearTimeout(highlightTimer.current) + } + }, []) + + useEffect(() => { + // Drop open state for cards that are hidden in half-meal mode + setOpenIds((current) => { + const next = new Set() + for (const id of current) { + if (visibleSteps.some((step) => step.n === id)) next.add(id) + } + if (next.size === 0 && visibleSteps[0]) next.add(visibleSteps[0].n) + return next + }) + }, [visibleSteps]) + + function focusStep(n: number) { + setOpenIds((current) => new Set(current).add(n)) + setHighlightId(n) + if (highlightTimer.current) clearTimeout(highlightTimer.current) + highlightTimer.current = setTimeout(() => setHighlightId(null), 300) + + requestAnimationFrame(() => { + document + .getElementById(`plate-step-${n}`) + ?.scrollIntoView({ behavior: "smooth", block: "nearest" }) + }) + } + + function toggleStep(n: number) { + setOpenIds((current) => { + const next = new Set(current) + if (next.has(n)) next.delete(n) + else next.add(n) + return next + }) + } + + const rules = [ui.plate.rule1, ui.plate.rule2, ui.plate.rule3, ui.plate.rule4] + + return ( +
+
+

{t(ui.plate.title, lang)}

+

+ {t(ui.plate.subtitle, lang)} +

+
+ + + +
+ focusStep(Number(id))} + /> + +
    + {visibleSteps.map((step) => ( +
  • + +
  • + ))} +
+
+ +
+ {visibleSteps.map((step) => ( +
+ toggleStep(step.n)} + lang={lang} + /> +
+ ))} +
+ +
+

+ {t(ui.plate.rulesTitle, lang)} +

+
    + {rules.map((rule, index) => ( +
  • + + — + + {t(rule, lang)} +
  • + ))} +
+
+
+ ) +} diff --git a/client/src/screens/SettingsScreen.tsx b/client/src/screens/SettingsScreen.tsx new file mode 100644 index 0000000..e2edf47 --- /dev/null +++ b/client/src/screens/SettingsScreen.tsx @@ -0,0 +1,58 @@ +import { Button } from "@/components/ui-pp/Button" +import { GlassCard } from "@/components/ui-pp/GlassCard" +import { t, ui } from "@/i18n/ui" +import { useLanguage } from "@/lib/language" +import { cx } from "@/lib/cx" +import type { Language } from "@/types/domain" + +const OPTIONS: { id: Language; label: string }[] = [ + { id: "de", label: "Deutsch" }, + { id: "en", label: "English" }, +] + +export function SettingsScreen() { + const { lang, setLang } = useLanguage() + + return ( + +

+ {t(ui.stubs.settingsTitle, lang)} +

+ +
+

+ {t(ui.stubs.language, lang)} +

+
+ {OPTIONS.map((option) => ( + + ))} +
+
+ +
+ +
+
+ ) +} diff --git a/client/src/screens/WeekPlanScreen.tsx b/client/src/screens/WeekPlanScreen.tsx new file mode 100644 index 0000000..b6f5513 --- /dev/null +++ b/client/src/screens/WeekPlanScreen.tsx @@ -0,0 +1,136 @@ +import { useMemo, useState } from "react" + +import { Accordion, NestedDisclosure } from "@/components/ui-pp/Accordion" +import { t, tr, ui } from "@/i18n/ui" +import { PLAN_TYPE_ORDER, recipesByType, resolveRecipeUses } from "@/lib/catalog" +import { useLanguage } from "@/lib/language" +import { cx } from "@/lib/cx" +import type { PlanRecipeType, Recipe } from "@/types/domain" + +const TYPE_ICON: Record = { + breakfast: "🌅", + snack: "🥤", + full: "🍽️", + recipe: "📖", +} + +function typeLabel(type: PlanRecipeType, lang: "de" | "en"): string { + return t(ui.weekplan[type], lang) +} + +function RecipeBody({ recipe, lang }: { recipe: Recipe; lang: "de" | "en" }) { + const ingredients = resolveRecipeUses(recipe) + + return ( +
+ {recipe.cooked !== undefined ? ( +

+ {recipe.cooked ? t(ui.weekplan.cooked, lang) : t(ui.weekplan.cold, lang)} +

+ ) : null} + + {recipe.text ? ( +

{tr(recipe.text, lang)}

+ ) : null} + + {recipe.variations && recipe.variations.length > 0 ? ( +
+

+ {t(ui.weekplan.variations, lang)} +

+
    + {recipe.variations.map((variation, index) => ( +
  • + + · + + {tr(variation, lang)} +
  • + ))} +
+
+ ) : null} + +
+

+ {t(ui.weekplan.uses, lang)} +

+
    + {ingredients.map((item) => ( +
  • + {tr(item.name, lang)} + {item.source === "extra" ? ( + + + ) : null} +
  • + ))} +
+
+
+ ) +} + +export function WeekPlanScreen() { + const { lang } = useLanguage() + const [openTypes, setOpenTypes] = useState>({ + breakfast: true, + snack: false, + full: false, + recipe: false, + }) + + const groups = useMemo( + () => + PLAN_TYPE_ORDER.map((type) => ({ + type, + items: recipesByType(type), + })), + [], + ) + + return ( +
+
+

+ {t(ui.weekplan.title, lang)} +

+

+ {t(ui.weekplan.intro, lang)} +

+
+ +
+ {groups.map(({ type, items }) => ( + + setOpenTypes((current) => ({ ...current, [type]: open })) + } + title={typeLabel(type, lang)} + meta={items.length} + icon={{TYPE_ICON[type]}} + > +
+ {items.map((recipe) => ( + + + + ))} +
+
+ ))} +
+
+ ) +} diff --git a/client/src/styles/glass.css b/client/src/styles/glass.css new file mode 100644 index 0000000..aa05527 --- /dev/null +++ b/client/src/styles/glass.css @@ -0,0 +1,29 @@ +/** + * Modal-only glass utilities. + * + * Home/surfaces use the elevated system in theme.css. + * True low-opacity glass is reserved for overlays/modals so content + * behind them stays readable without fighting the page gradient. + */ + +:root { + --modal-glass-fill: rgba(255, 255, 255, 0.12); + --modal-glass-blur: 16px; + --modal-glass-border: rgba(255, 255, 255, 0.35); + --modal-glass-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + --modal-scrim: rgba(25, 24, 0, 0.45); +} + +.modal-glass { + background: var(--modal-glass-fill); + backdrop-filter: blur(var(--modal-glass-blur)); + -webkit-backdrop-filter: blur(var(--modal-glass-blur)); + border: 1px solid var(--modal-glass-border); + box-shadow: var(--modal-glass-shadow); +} + +.modal-scrim { + background: var(--modal-scrim); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} diff --git a/client/src/styles/theme.css b/client/src/styles/theme.css new file mode 100644 index 0000000..be7d399 --- /dev/null +++ b/client/src/styles/theme.css @@ -0,0 +1,270 @@ +/** + * Pocket Pascal — Elevated Surface Design Tokens + * + * Pivoted from heavy glassmorphism to a low-transparency, tactile system: + * cards read as solid raised surfaces (elevation shadow + faint top/bottom + * bevel), buttons visibly depress on press (inset shadow + 1px downward + * shift) instead of just scaling. `--glass-*` names kept for minimal diff + * across components — they now mean "surface fill / surface blur", not + * "see-through glass". Blur is low (8px) and opacity high (~88%): it softens + * whatever sits behind a card without reading as transparent. + * + * Toggle dark mode by adding/removing the `dark` class on + * (matches Tailwind `darkMode: 'class'` — see README). + * + * Brand hues (protein/obstgem/fett/kh + ink) come from the WCAG-audited + * Pocket Pascal palette, not generic placeholders — see /docs/CONCEPT.html + * section 05/08 for the full rationale and contrast numbers. + */ + +:root { + /* ---------- Elevated surface (formerly "glass") ---------- */ + --glass-fill: rgba(255, 255, 255, 0.9); + --glass-blur: 8px; + /* top-light / bottom-shadow bevel instead of an even glass edge — reads as raised, not see-through */ + --glass-border-from: rgba(255, 255, 255, 0.9); + --glass-border-to: rgba(0, 0, 0, 0.06); + + /* ---------- Radius scale (nested: outer ≈ inner + padding) ---------- */ + --radius-card: 24px; + --radius-nested: 12px; + --radius-control: 16px; + --radius-pill: 9999px; + + /* ---------- Shadows (resting → elevated → float; warm ink-tinted ambient) ---------- */ + --shadow-resting: 0 1px 2px rgba(25, 24, 0, 0.04), 0 1px 3px rgba(25, 24, 0, 0.06); + --shadow-elevated: 0 2px 4px rgba(25, 24, 0, 0.04), 0 8px 20px rgba(25, 24, 0, 0.08); + --shadow-float: 0 4px 8px rgba(25, 24, 0, 0.05), 0 16px 32px rgba(25, 24, 0, 0.1); + --shadow-pressed: inset 0 2px 5px rgba(25, 24, 0, 0.22), inset 0 1px 1px rgba(25, 24, 0, 0.12); + /* BottomNav / overlays sit one step above content cards */ + --shadow-ambient: var(--shadow-float); + + /* ---------- Brand ink / neutrals ---------- */ + --ink: #191800; + --ink-soft: #5c5b48; + --ink-faint: #6b6a56; + --border: #e4e2d8; + + /* ---------- Brand category colors (bright primary + dark accent pairs) ---------- */ + --color-protein: #d9f227; + --color-protein-dk: #2e3200; + --color-obstgem: #ff6a3d; + --color-obstgem-dk: #3d1508; + --color-fett: #ffb300; + --color-fett-dk: #3d2900; + --color-kh: #1fd9c4; + --color-kh-dk: #04302b; + --color-pick: #0f7a37; + --color-caution: #c81e1e; + + /* ---------- Plate / Baukasten category hues (from prototype) ---------- */ + --green: #2f5233; + --green-soft: #e3ece4; + --terracotta: #b33d22; + --terracotta-soft: #f7e0d8; + --gold: #e8b93a; + --gold-soft: #fdf1d9; + --slate: #6b4c9a; + --slate-soft: #efe9f5; + + /* ---------- Surface gradients (subtle, sit UNDER glass layers) ---------- */ + --surface-gradient: linear-gradient(160deg, #fafaf7 0%, #f0f0ea 100%); + --page-gradient: radial-gradient(120% 100% at 0% 0%, #fff9e0 0%, #ffffff 45%), + radial-gradient(120% 100% at 100% 100%, #e2fbf6 0%, #ffffff 45%); + + /* ---------- Action gradients (vibrant, multi-stop) ---------- */ + /* NOTE: brand hues are light/high-luminance by design (see contrast audit), + so action gradients pair with --ink text, not white — this keeps every + button at AA contrast without text-shadow hacks. Do not swap to white + text on these without re-checking contrast. */ + --gradient-primary: linear-gradient(135deg, #d9f227 0%, #1fd9c4 100%); + --gradient-accent: linear-gradient(135deg, #ff6a3d 0%, #ffb300 100%); + --gradient-caution: linear-gradient(135deg, #ff6a3d 0%, #c81e1e 100%); + + /* ---------- Dining Plate chart tokens ---------- */ + --plate-rim-light: #ffffff; + --plate-rim-shadow: #d8d8d0; + --plate-well-center: #f7f7f2; + --plate-well-edge: #e6e6dd; +} + +.dark { + --glass-fill: rgba(28, 28, 21, 0.88); + --glass-blur: 8px; + --glass-border-from: rgba(255, 255, 255, 0.1); + --glass-border-to: rgba(0, 0, 0, 0.35); + + --shadow-resting: 0 1px 2px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.25); + --shadow-elevated: 0 2px 4px rgba(0, 0, 0, 0.35), 0 10px 24px rgba(0, 0, 0, 0.4); + --shadow-float: 0 4px 8px rgba(0, 0, 0, 0.4), 0 18px 36px rgba(0, 0, 0, 0.45); + --shadow-pressed: inset 0 2px 5px rgba(0, 0, 0, 0.55), inset 0 1px 1px rgba(0, 0, 0, 0.3); + --shadow-ambient: var(--shadow-float); + + --ink: #f3f2e6; + --ink-soft: #c2c1ac; + --ink-faint: #8f8e7a; + --border: #33342a; + + /* category primaries stay identical (they're the brand, not a theme) */ + --color-protein: #d9f227; + --color-protein-dk: #2e3200; + --color-obstgem: #ff6a3d; + --color-obstgem-dk: #3d1508; + --color-fett: #ffb300; + --color-fett-dk: #3d2900; + --color-kh: #1fd9c4; + --color-kh-dk: #04302b; + --color-pick: #4ade80; + --color-caution: #ff6b60; + + --surface-gradient: linear-gradient(160deg, #1c1c15 0%, #14140f 100%); + --page-gradient: radial-gradient(120% 100% at 0% 0%, #2a2a12 0%, #14140f 45%), + radial-gradient(120% 100% at 100% 100%, #0f2a26 0%, #14140f 45%); + + --gradient-primary: linear-gradient(135deg, #d9f227 0%, #1fd9c4 100%); + --gradient-accent: linear-gradient(135deg, #ff6a3d 0%, #ffb300 100%); + --gradient-caution: linear-gradient(135deg, #ff6a3d 0%, #ff6b60 100%); + + --plate-rim-light: #2a2a22; + --plate-rim-shadow: #0c0c09; + --plate-well-center: #1c1c15; + --plate-well-edge: #111109; +} + +/* ---------- Fonts ---------- */ +/* Download the two static woff2 files listed in README into /public/fonts, + then this block just works. Using local @font-face (not next/font) keeps + this file framework-agnostic for Cursor. */ +@font-face { + font-family: "Unbounded"; + font-weight: 700; + font-style: normal; + font-display: swap; + src: url("/fonts/unbounded-700.woff2") format("woff2"); +} +@font-face { + font-family: "Unbounded"; + font-weight: 900; + font-style: normal; + font-display: swap; + src: url("/fonts/unbounded-900.woff2") format("woff2"); +} +@font-face { + font-family: "Albert Sans"; + font-weight: 400; + font-style: normal; + font-display: swap; + src: url("/fonts/albert-sans-400.woff2") format("woff2"); +} +@font-face { + font-family: "Albert Sans"; + font-weight: 500; + font-style: normal; + font-display: swap; + src: url("/fonts/albert-sans-500.woff2") format("woff2"); +} +@font-face { + font-family: "Albert Sans"; + font-weight: 700; + font-style: normal; + font-display: swap; + src: url("/fonts/albert-sans-700.woff2") format("woff2"); +} + +.font-display { + font-family: "Unbounded", ui-sans-serif, sans-serif; + letter-spacing: -0.01em; +} +.font-body { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; +} + +/* ---------- Type ramp (single hierarchy) ---------- */ +.type-display { + font-family: "Unbounded", ui-sans-serif, sans-serif; + font-weight: 900; + font-size: 1.5rem; /* 24px */ + line-height: 1.1; + letter-spacing: -0.01em; +} +.type-title { + font-family: "Unbounded", ui-sans-serif, sans-serif; + font-weight: 700; + font-size: 1.25rem; /* 20px */ + line-height: 1.2; + letter-spacing: -0.01em; +} +.type-title-sm { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + font-weight: 700; + font-size: 1rem; /* 16px */ + line-height: 1.3; +} +.type-body { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + font-weight: 400; + font-size: 0.875rem; /* 14px */ + line-height: 1.5; +} +.type-body-emphasis { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + font-weight: 700; + font-size: 0.875rem; + line-height: 1.35; +} +.type-label { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + font-weight: 700; + font-size: 0.6875rem; /* 11px */ + line-height: 1.3; + letter-spacing: 0.1em; + text-transform: uppercase; +} +.type-caption { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + font-weight: 400; + font-size: 0.75rem; /* 12px */ + line-height: 1.45; +} +.type-button { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + font-weight: 700; + font-size: 0.875rem; + line-height: 1.2; +} +.type-button-secondary { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; + font-weight: 500; /* closest to semibold until 600 is loaded */ + font-size: 0.875rem; + line-height: 1.2; +} + +/* ---------- Glass border (gradient 1px border that respects border-radius) ---------- */ +/* border-image ignores border-radius, so we use a masked pseudo-element instead. + Apply `.glass-border` to a `relative` container with `overflow-hidden` off + (the pseudo-element is inset:0 and radius:inherit). */ +.glass-border { + border-radius: inherit; + padding: 1px; + /* top-light, bottom-shadow — reads as a raised bevel, not a glass sheen */ + background: linear-gradient(180deg, var(--glass-border-from), var(--glass-border-to)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; +} + +/* Applied on top of --shadow-elevated/--shadow-resting to animate between them. */ +.elevation-transition { + transition: box-shadow 120ms ease-out, transform 120ms ease-out, filter 120ms ease-out; +} + +/* ---------- PWA polish ---------- */ +html, +body { + -webkit-tap-highlight-color: transparent; + overscroll-behavior-y: contain; +} +.no-select { + -webkit-user-select: none; + user-select: none; +} diff --git a/client/src/types/domain.ts b/client/src/types/domain.ts new file mode 100644 index 0000000..1fd8b58 --- /dev/null +++ b/client/src/types/domain.ts @@ -0,0 +1,79 @@ +export type Language = "de" | "en" + +export interface LocalizedString { + de: string + en: string +} + +export type CategoryId = + | "protein" + | "snack" + | "obst" + | "gemuese" + | "fett" + | "kh" + +export type TagType = "pick" | "caution" + +export type MealType = "full" | "half" + +export type TimeSlot = "morning" | "midday" | "afternoon" | "evening" + +export type PlanRecipeType = "breakfast" | "snack" | "full" | "recipe" + +export type HomeWidgetId = "suggestion" | "knowledge" | "builder" + +export type WidgetOrder = HomeWidgetId[] + +export interface FoodItem { + id: string + cat: CategoryId + name: LocalizedString + valueInfo: LocalizedString + tag?: TagType + isApprox?: boolean +} + +export interface FoodCategory { + id: CategoryId + cls: string + title: LocalizedString + thumb: LocalizedString + items: FoodItem[] +} + +/** Condiment / prep ingredient outside the plate categories (CATS). */ +export interface ExtraItem { + id: string + name: LocalizedString +} + +/** Resolved CATS or EXTRAS entry for recipe `uses` ids. */ +export interface CatalogItem { + id: string + name: LocalizedString + source: "cat" | "extra" + cat?: CategoryId + valueInfo?: LocalizedString + tag?: TagType +} + +/** + * Curated meal / snack / how-to recipes. + * `uses` references ids from CATS (foods) or EXTRAS. + */ +export interface Recipe { + id: string + type: PlanRecipeType + name: LocalizedString + uses: string[] + cooked?: boolean + text?: LocalizedString + /** Optional flavor / protein twists shown under the base recipe text. */ + variations?: LocalizedString[] +} + +export interface BuilderHandoffState { + ingredientIds: string[] + source?: "suggestion" | "home" +} diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index f01cf23..580a24f 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -13,6 +13,7 @@ "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", + "resolveJsonModule": true, "noEmit": true, "jsx": "react-jsx", diff --git a/client/vite.config.ts b/client/vite.config.ts index cbb2bce..7ae7451 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -15,9 +15,9 @@ export default defineConfig({ manifest: { name: 'Pocket Pascal', short_name: 'Pocket Pascal', - description: 'A simple Hello World PWA with a React frontend and SQLite backend.', - theme_color: '#0a0a0a', - background_color: '#0a0a0a', + description: 'Nutrition decision aid based on Pascal’s building-block plate system.', + theme_color: '#fafaf7', + background_color: '#fafaf7', display: 'standalone', start_url: '/', icons: [ diff --git a/handover/CLAUDE.md b/handover/CLAUDE.md new file mode 100644 index 0000000..04c1c3c --- /dev/null +++ b/handover/CLAUDE.md @@ -0,0 +1,43 @@ +# Pocket Pascal + +Nutrition coach app for Steffi + David, built on Pascal Wirth's Ernährungsbaukasten (building-block nutrition system). Decision aid for "what do I eat right now" — not a tracker, not chat-first. See `/docs/CONCEPT.md` for the full spec. + +## Before touching this repo + +Read `/docs/CONCEPT.md` in full. It is the source of truth for: +- Vision, non-goals, and the "app should make itself obsolete" success criterion +- The plate/building-block rules (protein, fat, carbs, veg/fruit) and portion sizes +- The module list and which phase each belongs to +- The decision log (section 8) — settled calls, do not re-litigate without a documented reason +- Open questions (section 7) — several are marked as UX calls Steffi owns; don't decide those unilaterally + +## Platform + +Web/PWA. **Not** a native iOS app. Decided 2026-08-03 (see decision log) — only 2 users, no App Store needed, and the existing vanilla JS data model/logic carries over directly. + +Target stack: React + shadcn/ui + Tailwind, hosted on Vercel or GitHub Pages (exact host still an open question — see concept doc §7). + +## Data model + +`/data/foods.json` and `/data/steps.json` are extracted from the working prototype. Structured records, not display strings: + +```json +{ "id": "chicken_breast", "cat": "protein", + "name": { "de": "Hühnerbrust", "en": "Chicken breast" }, + "v": { "de": "111 kcal · 24 g", "en": "111 kcal · 24 g" }, + "f": "pick" } +``` + +`f` is `"pick"` (especially recommended), `"caution"` (enjoy mindfully), or absent. `v` is currently a display string per item — **not yet normalized to a common basis**. Pantry items are mostly per-100g; snack items are per-serving. Normalizing everything to per-100g is a prerequisite before quantities/calorie math can be added (see concept doc, Teller-Builder — Analyse-Layer module). + +DE is the primary working language (Steffi edits it directly), EN is the general template — every user-facing string needs both. UI chrome strings (not food data) live in a separate dictionary, not duplicated HTML. + +## What's already built (reference only — not authoritative, rebuild in the new stack) + +`/reference/kueche_app.html` — working single-file prototype (Claude Artifact). Has: plate rule view, pantry/knowledge base with search, Teller-Builder (multi-select plate builder with pick/caution scoring), settings with language switch. Useful for behavior reference, not for copy-pasting into the new stack as-is. + +## Working conventions + +- Bilingual: every content change ships both `de` and `en` in the same commit. +- Curated, not exhaustive — see non-goals in the concept doc. Don't add a food-database API integration without checking §7 first. +- UX/UI decisions are Steffi's call (she's the UX designer on this project). Flag them, don't just decide. diff --git a/handover/CONCEPT.html b/handover/CONCEPT.html new file mode 100644 index 0000000..f09ae42 --- /dev/null +++ b/handover/CONCEPT.html @@ -0,0 +1,665 @@ +Pocket Pascal — Konzeptdokument + + +
+ +
+ + +
+ +
+ Arbeitsdokument · internWorking document · internal +

Pocket Pascal

+

+ Ein Ernährungs-Coach für die Hosentasche. Verhindert schlechte Entscheidungen im Moment — statt Gewohnheiten zu überwachen. + A nutrition coach for your pocket. Prevents poor decisions in the moment — instead of monitoring habits. +

+
+ StandVersion 2026-08-03 + AutorenAuthors Steffi + Claude + Status Entwurf, Phase 1 offenDraft, Phase 1 open + SprachenLanguages DE (primär) / EN +
+
+ + +
+

01VisionVision

+

+ Was die App ist — und vor allem, was sie nicht ist. + What the app is — and more importantly, what it is not. +

+ +

+ Pocket Pascal ist eine Entscheidungshilfe für den Moment: „Es ist 16 Uhr, ich habe Hunger, was jetzt?" Die App beantwortet das in wenigen Taps, gestützt auf Pascals Ernährungsbaukasten und den vorhandenen Vorrat. + Pocket Pascal is a decision aid for the moment: "It's 4pm, I'm hungry, what now?" The app answers that in a few taps, based on Pascal's building-block system and what's actually in the kitchen. +

+

+ Das Erfolgskriterium ist ungewöhnlich: Die App soll sich überflüssig machen. Wenn die Daumenregeln verinnerlicht sind, wird sie seltener gebraucht. Das ist gewollt, kein Fehler. + The success criterion is unusual: the app should make itself obsolete. Once the rules of thumb are internalised, it gets used less. That's intended, not a flaw. +

+ +

Nicht-ZieleNon-goals

+
    +
  • Kein Tracker. Kein Kalorienzählen, keine Streaks, keine Schuldgefühl-Mechanik. Protokolliert wird im Coaching-Tool (Nutrilize), nicht hier.Not a tracker. No calorie counting, no streaks, no guilt mechanics. Logging happens in the coaching tool (Nutrilize), not here.
  • +
  • Kein Chat-first-Interface. Buttons und klare UI schlagen offene Konversation. Ausnahme: tiefe Manager-Module (Kühlschrank, Wochenplanung) in Phase 3.Not chat-first. Buttons and clear UI beat open-ended conversation. Exception: deeper manager modules (fridge, weekly planning) in Phase 3.
  • +
  • Kein Ersatz für den Coach. Die App ist Nachschlagewerk und Anwendungshilfe für Pascals Vorgaben, keine eigene Ernährungsberatung.Not a replacement for the coach. The app applies Pascal's guidance; it does not give its own nutritional advice.
  • +
  • Keine Vollständigkeit. Lieber 40 gute Rezepte als 4.000 mittelmäßige. Der Datensatz bleibt kuratiert.Not exhaustive. Better 40 good recipes than 4,000 mediocre ones. The dataset stays curated.
  • +
+ +
+ KernspannungCore tension + Flexibilität gegen Struktur. Zu starr → wird umgangen. Zu offen → gibt keine Antwort und wird nicht geöffnet. Jede Funktion muss sich an dieser Achse messen lassen. + Flexibility versus structure. Too rigid → gets circumvented. Too open → gives no answer and doesn't get opened. Every feature has to be measured against this axis. +
+
+ + +
+

02Nutzer & BedürfnisseUsers & needs

+

+ Zwei Personen, zwei sehr unterschiedliche Einstiegspunkte in dieselbe Logik. + Two people, two very different entry points into the same logic. +

+ +
+ + + + + + + + + + + + + + + + + + + + +
NutzerUserHauptbedürfnisPrimary needBedient durchServed by
SteffiRepertoire erweitern. Weiß bei manchen Zutaten nicht, was sie damit anfangen soll (z. B. Proteinpulver). Will Neues ausprobieren und eine mentale Rezeptbibliothek aufbauen.Broaden her repertoire. Doesn't know what to do with certain ingredients (e.g. protein powder). Wants to try new things and build a mental recipe library.Rezeptbibliothek, VorschlagsmaschineRecipe library, suggestion engine
DavidEinkaufen mit richtiger Kategorie-Balance. Braucht (a) ein Gefühl für Lebensmittelkategorien im Laden und (b) Ideen für das, was schon zu Hause ist.Shopping with the right category balance. Needs (a) an intuitive sense for food categories while shopping and (b) ideas for what's already at home.Einkaufshilfe, Knowledge Base, KühlschrankShopping aid, knowledge base, fridge
+
+ +

+ Beide Bedürfnisse laufen auf dieselbe Datenschicht hinaus: Lebensmittel mit Kategorie, Rezepte als Kombinationen daraus. Der Unterschied liegt nur in der Richtung der Abfrage — „was koche ich aus X?" gegen „was muss ich für die Woche kaufen?". + Both needs resolve to the same data layer: foods with a category, recipes as combinations of them. The only difference is the direction of the query — "what do I cook from X?" versus "what do I need to buy for the week?". +

+
+ + +
+

03Plate Logic — das RegelwerkPlate logic — the rule set

+

+ Der inhaltliche Kern. Stammt aus Pascals Ernährungsbaukasten und Einkaufsguide. + The substantive core. Sourced from Pascal's building-block system and shopping guide. +

+ +

Baukasten-ReihenfolgeBuilding-block sequence

+
    +
  • 1. Protein — 30 g pro Mahlzeit, 150 g am Tag1. Protein — 30 g per meal, 150 g per day
  • +
  • 2. Obst oder Gemüse — eine Portion (Obst 100 g, Gemüse 200 g)2. Fruit or vegetables — one portion (fruit 100 g, veg 200 g)
  • +
  • 3. Fett — 15 g Kerne/Nüsse, ¼ Avocado oder 1 EL Öl3. Fat — 15 g seeds/nuts, ¼ avocado or 1 tbsp oil
  • +
  • 4. Kohlenhydrate — 75 g trocken, 250 g Kartoffeln oder 50 g Haferflocken4. Carbohydrates — 75 g dry, 250 g potatoes or 50 g oats
  • +
+ +

TellerverteilungPlate distribution

+
    +
  • Ganze Mahlzeit — ⅓ Protein, ⅓ Gemüse/Obst, ⅓ Fett + KohlenhydrateFull meal — ⅓ protein, ⅓ veg/fruit, ⅓ fat + carbs
  • +
  • Halbe Mahlzeit (Snack) — Protein + Gemüse/Obst, ohne KohlenhydrateHalf meal (snack) — protein + veg/fruit, no carbs
  • +
+ +

TagesstrukturDay structure

+

+ 4 Mahlzeiten, ca. alle 3–4 Std. Morgens Protein + Obst · mittags ganzer Teller · nachmittags Proteinsnack + Obst/Gemüse · abends ganzer Teller. 2 L Wasser täglich. + 4 meals, roughly every 3–4 hours. Morning protein + fruit · midday full plate · afternoon protein snack + fruit/veg · evening full plate. 2 L water daily. +

+

+ Diese Struktur ist zugleich die Vorschlagslogik: Aus der Uhrzeit folgt der Mahlzeitentyp, aus dem Typ folgt die Zusammensetzung. Kein Rätselraten nötig. + This structure doubles as the suggestion logic: time of day determines the meal type, meal type determines the composition. No guesswork needed. +

+ +

EinkaufsregelShopping rule

+

+ Pro Einkauf mindestens 5 Proteinquellen, 5 Obst-/Gemüsesorten, 4 Fettquellen, 4 Kohlenhydratquellen. Bedarf = Tage bis zum nächsten Einkauf × Mahlzeiten pro Tag. Diese Formel ist direkt rechenbar und wird zur Einkaufshilfe. + Per shopping trip: at least 5 protein sources, 5 fruit/veg varieties, 4 fat sources, 4 carb sources. Demand = days until next trip × meals per day. This formula is directly computable and becomes the shopping aid. +

+ +

Daumenregeln (Etikett-Check)Rules of thumb (label check)

+
    +
  • Protein — mindestens 15–20 g pro 100 g, wenig Zucker & Fett, kurze ZutatenlisteProtein — at least 15–20 g per 100 g, low sugar & fat, short ingredient list
  • +
  • Kohlenhydrate — Vollkorn als erste Zutat; Faustregel 10:1 (pro 10 g KH mind. 1 g Ballaststoffe)Carbs — whole grain as first ingredient; 10:1 rule (at least 1 g fibre per 10 g carbs)
  • +
  • Fett — native/kaltgepresste Öle, ungesalzene Nüsse, Zutatenliste = nur die ZutatFat — native/cold-pressed oils, unsalted nuts, ingredient list = just the ingredient
  • +
  • Zucker — Tagesbudget = kcal × 10 % ÷ 4,1. Versteckt hinter „-ose", „…zucker", „…sirup"Sugar — daily budget = kcal × 10% ÷ 4.1. Hidden behind "-ose", "…sugar", "…syrup"
  • +
+
+ + +
+

04ModuleModules

+

+ Sieben Bausteine, nach Phase sortiert. Jeder für sich nutzbar. + Seven building blocks, sorted by phase. Each usable on its own. +

+ +
+
+
+ Knowledge BaseKnowledge base + StehtBuilt + Phase 1 +
+

Teller-Regel und Vorratskammer als Nachschlagewerk: Kategorien aufklappbar, Suche, Punkte-Markierung (empfehlenswert / bewusst genießen), Daumenregeln pro Kategorie. Das Fundament, auf das alle anderen Module zugreifen. Muss auf das strukturierte Datenmodell umgestellt werden.Plate rule and pantry as a reference: collapsible categories, search, dot markers (recommended / enjoy mindfully), rules of thumb per category. The foundation all other modules read from. Needs migrating to the structured data model.

+
+ +
+
+ RezeptbibliothekRecipe library + Phase 1 +
+

Kuratierte Sammlung von Mahlzeiten als Kombinationen aus Baukasten-Zutaten. Jedes Rezept kennt seine Zutaten-IDs, den Mahlzeitentyp und die Tageszeit. Startbestand: die 13 Beispielmahlzeiten aus der Ernährungscheckliste, danach schrittweise erweitert. Ohne diese Schicht kann die Vorschlagsmaschine nur die Tellerverteilung wiederholen.A curated collection of meals as combinations of building-block ingredients. Each recipe knows its ingredient IDs, meal type and time of day. Seed: the 13 example meals from the nutrition checklist, expanded gradually. Without this layer the suggestion engine can only repeat the plate ratio.

+
+ +
+
+ VorschlagsmaschineSuggestion engine + Phase 1 +
+

„Was esse ich jetzt?" Ein Tap. Uhrzeit bestimmt den Vorschlag (halbe oder ganze Mahlzeit), manuelle Umschaltung bleibt möglich. Zieht passende Rezepte aus der Bibliothek. Rein regelbasiert, keine KI nötig. Später filterbar nach Kühlschrankinhalt."What do I eat now?" One tap. Time of day drives the suggestion (half or full meal), with manual override. Pulls matching recipes from the library. Purely rule-based, no AI needed. Later filterable by fridge contents.

+
+ +
+
+ Teller-BuilderPlate builder + StehtBuilt + Phase 1 +
+

Pro Kategorie (Protein, Fett, Kohlenhydrat, Obst/Gemüse) beliebig viele Lebensmittel als Chips antippen — die App bewertet die Kombination gegen die Baukasten-Regeln und die Pick/Caution-Markierungen: guter Teller, guter Snack, oder was fehlt. Kein Backend nötig, reine Client-Logik auf dem bestehenden Datenmodell. Übersetzt die Vorratskammer direkt in gefühltes Wissen — trifft die Vision „App macht sich überflüssig" am direktesten.Tap any number of foods per category (protein, fat, carb, veg/fruit) as chips — the app evaluates the combination against the building-block rules and the pick/caution tags: good plate, good snack, or what's missing. No backend needed, pure client-side logic on the existing data model. Translates the pantry straight into felt intuition — hits the "app makes itself obsolete" vision most directly.

+
+ +
+
+ Teller-Builder — Analyse-LayerPlate builder — analysis layer + Phase 2 +
+

Baut auf dem Teller-Builder auf, sobald Mengen dazukommen. Vier Parameter: Kalorien (Summe aus Mengen × Nährwert), Makros (Protein/Fett/KH gegen Baukasten-Zielwerte), Vielfalt (abgedeckte Vitamine/Mineralien je Teller, später auch über die Woche), Zucker (Tagesbudget-Regel aus dem Zucker-Guide). Mengen bekommen einen Vorschlagswert aus den Baukasten-Standardmengen (30 g Protein, 15 g Fett, 75 g KH, 100 g Obst/200 g Gemüse), editierbar. Ausgewählte Kombination als Favorit speichern — landet direkt in der Rezeptbibliothek, keine zweite Datenstruktur nötig. Voraussetzung: alle Lebensmittel auf eine einheitliche Basis (pro 100 g) normalisieren — aktuell sind Snacks pro Portion, der Rest pro 100 g angegeben.Builds on the plate builder once quantities are added. Four parameters: calories (sum of quantity × nutrition value), macros (protein/fat/carbs against building-block targets), variety (vitamins/minerals covered per plate, later across the week too), sugar (daily-budget rule from the sugar guide). Quantities get a suggested value from the building-block defaults (30 g protein, 15 g fat, 75 g carbs, 100 g fruit/200 g veg), editable. Save a combination as a favourite — it lands straight in the recipe library, no second data structure needed. Prerequisite: normalise all foods onto one basis (per 100 g) — currently snacks are per serving, everything else per 100 g.

+
+ +
+
+ Settings + Phase 1 +
+

Sprachumschaltung DE/EN, gespeichert. Später: Mahlzeiten pro Tag, Einkaufsrhythmus, Kalorienrichtwert für das Zuckerbudget.Language switch DE/EN, persisted. Later: meals per day, shopping rhythm, calorie reference for the sugar budget.

+
+ +
+
+ EinkaufshilfeShopping aid + Phase 2 +
+

Rechnet aus Einkaufsrhythmus und Mahlzeitenzahl den Bedarf je Kategorie und schlägt eine ausgewogene Liste vor. Abhakbar im Laden. Bedient primär David.Calculates per-category demand from shopping rhythm and meal count, then proposes a balanced list. Checkable in the shop. Primarily serves David.

+
+ +
+
+ Virtueller KühlschrankVirtual fridge + Phase 2 +
+

Erfasst, was da ist; bucht Zutaten ab, wenn gekocht wurde. Ermöglicht „was kann ich jetzt kochen?" und hilft, angebrochene Packungen über mehrere Tage aufzubrauchen (z. B. eine Dose Bohnen). Braucht Persistenz — technisch die erste echte Hürde. Später: speist die Chip-Auswahl im Teller-Builder direkt aus dem Bestand statt aus der ganzen Vorratskammer — dieselbe Bewertungslogik, andere Datenquelle.Records what's available; deducts ingredients once a meal is cooked. Enables "what can I make right now?" and helps use up opened items across several days (e.g. a can of beans). Requires persistence — the first real technical hurdle. Later: feeds the plate builder's chip choices straight from stock instead of the whole pantry — same scoring logic, different data source.

+
+ +
+
+ WunschlisteWishlist + Phase 2 +
+

Merkliste für Dinge, die in die Vorratskammer sollen, aber gerade fehlen — Bindeglied zwischen Kühlschrank und Einkaufsliste. Ohne eigene Logik: Einträge landen automatisch auf der nächsten Einkaufsliste.A running list of things that should be in the pantry but currently aren't — the link between the fridge and the shopping list. No logic of its own: entries land on the next shopping list automatically.

+
+ +
+
+ Konversationelle ManagerConversational managers + Phase 3 +
+

Kühlschrank-Manager und Wochenplaner mit natürlicher Sprache („hab noch Spinat und Feta, was geht?"). Die einzige Stelle, an der Chat sinnvoll ist — weil die Eingabe zu vielfältig für Buttons wird. Braucht Backend.Fridge manager and weekly planner with natural language ("got spinach and feta left, what works?"). The only place chat makes sense — because input becomes too varied for buttons. Requires a backend.

+
+
+ +

Zusammenspiel der Module — der KreislaufHow the modules interlock — the loop

+

+ Aus dem FigJam-Board: Die Module sind kein loses Set, sie bilden einen geschlossenen Kreislauf. Jeder Schritt füttert den nächsten, der letzte Schritt füttert wieder den ersten. + From the FigJam board: the modules aren't a loose set — they form a closed loop. Each step feeds the next, and the last step feeds back into the first. +

+ +
+
+
Kühlschrankkennt BestandFridgeknows stock
+
+
EinkaufslisteShopping list
+
+
Wochenplanmit Einkaufsliste verknüpftMeal planlinked to shopping list
+
+
RezeptvorschlägeRecipe suggestions
+
+
„Hab Hunger, was jetzt?""Hungry now, what's next?"
+
+
+ + Die Antwort verbraucht Zutaten aus dem Bestand — und schreibt zurück in den Kühlschrank. Der Kreis schließt sich.The answer consumes ingredients from stock — and writes back into the fridge. The loop closes. +
+
+ + Die Knowledge Base (Teller-Regel, Vorratskammer, Daumenregeln) liegt unter jedem einzelnen Schritt — jedes Modul liest daraus, statt eigene Regeln mitzubringen.The knowledge base (plate rule, pantry, rules of thumb) underlies every single step — each module reads from it instead of carrying its own rules. +
+
+ +
+ Konsequenz für die RoadmapConsequence for the roadmap + Kühlschrank (Phase 2) ist der Dreh- und Angelpunkt des Kreislaufs, nicht nur ein Feature unter anderen. Einkaufshilfe und Vorschlagsmaschine bekommen ihren vollen Wert erst, wenn er steht. Phase 1 liefert die Module isoliert nutzbar vor — der Kreislauf selbst ist Phase 2. + The fridge (Phase 2) is the pivot of the loop, not just one feature among others. The shopping aid and suggestion engine only reach their full value once it exists. Phase 1 ships the modules usable in isolation — the loop itself is Phase 2. +
+
+ + +
+

05ArchitekturArchitecture

+

+ Entscheidungen, die jetzt getroffen werden müssen, weil sie später teuer sind. + Decisions that must be made now because they get expensive later. +

+ +

Datenmodell: strukturiert statt Anzeige-StringsData model: structured, not display strings

+

+ Der aktuelle Prototyp speichert fertige Anzeigetexte. Das blockiert sowohl Zweisprachigkeit als auch jede Rechenfunktion. Neue Form: + The current prototype stores ready-made display text. That blocks both bilingualism and any computation. New shape: +

+
// Lebensmittel / food item
+{ id: 'chicken_breast',
+  cat: 'protein',
+  name:   { de: 'Hühnerbrust', en: 'Chicken breast' },
+  kcal:   111,
+  protein: 24,
+  tag:    'pick' }          // 'pick' | 'caution' | null
+
+// Rezept / recipe
+{ id: 'eggs_cottage_cucumber',
+  name:  { de: '3 Eier + Hüttenkäse + Gurke', en: '3 eggs + cottage cheese + cucumber' },
+  type:  'full',            // 'full' | 'half'
+  times: ['midday','evening'],
+  uses:  ['egg','cottage_cheese','cucumber','lentil_waffle'] }
+

+ Damit wird alles möglich: Übersetzen (name[lang]), Rechnen (Einkaufsmengen), Filtern (welches Rezept passt zum Vorrat), Verknüpfen (Rezept ↔ Zutat ↔ Kategorie). + This unlocks everything: translation (name[lang]), computation (shopping quantities), filtering (which recipe fits the stock), linking (recipe ↔ ingredient ↔ category). +

+ +
+ Jetzt umstellenMigrate now + Der Datensatz umfasst aktuell rund 70 Einträge. Bei 300 wird der Umbau zur Tagesaufgabe. Der Refactor gehört an den Anfang von Phase 1, nicht ans Ende. + The dataset currently holds around 70 entries. At 300 the migration becomes a full day's work. The refactor belongs at the start of Phase 1, not the end. +
+ +

PortabilitätPortability

+

+ Ziel ist der spätere Umzug auf echtes Hosting. Damit das kein Neubau wird, gilt ab sofort: Daten, Logik und Darstellung strikt getrennt. Die Datenschicht bleibt reines JavaScript ohne DOM-Bezug — sie lässt sich unverändert in ein Repo übernehmen. Keine Abhängigkeit von Artifact-spezifischen APIs im Kern. + The goal is an eventual move to real hosting. To keep that from becoming a rebuild, from now on: strictly separate data, logic and presentation. The data layer stays plain JavaScript with no DOM references — it can move into a repo untouched. No dependency on Artifact-specific APIs in the core. +

+ +
+ + + + + + + + + + + + + + + + +
FähigkeitCapabilityArtifactEigenes HostingOwn hosting
Nachschlagen, Rechnen, VorschlägeReference, computation, suggestions
Home-Bildschirm-IconHome screen iconüber Safari „Teilen"via Safari "Share"
Zustand speichern (Kühlschrank, Sprache)Persisted state (fridge, language)ungeprüftunverified
Offline-NutzungOffline use
Konversationelle ModuleConversational modules
Sync zwischen zwei NutzernSync between two users
+
+ +

ZweisprachigkeitBilingualism

+

+ DE ist die primäre Arbeitssprache, EN die allgemeine Vorlage. Umsetzung nicht mehr über doppelte HTML-Blöcke wie bisher, sondern über ein t()-Lookup gegen das Datenmodell — sonst wächst jede Seite doppelt. UI-Texte kommen in ein eigenes Wörterbuch. + DE is the primary working language, EN the general template. No longer implemented via duplicated HTML blocks, but through a t() lookup against the data model — otherwise every page doubles in size. UI strings move into their own dictionary. +

+
+ + +
+

06Roadmap

+

+ Phase 1 muss allein stehen können. Erst wenn sie im Alltag benutzt wird, kommt Phase 2. + Phase 1 has to stand on its own. Phase 2 only follows once Phase 1 is actually used day to day. +

+ +

Phase 1 — FundamentPhase 1 — foundation

+
    +
  • Datenmodell-Refactor: Anzeige-Strings → strukturierte Records mit IDs und de/enData model refactor: display strings → structured records with IDs and de/en
  • +
  • Settings-Seite mit Sprachumschaltung; localStorage im Artifact prüfenSettings page with language switch; verify localStorage inside the Artifact
  • +
  • Knowledge Base auf das neue Modell umstellen (Teller-Regel + Vorratskammer + Daumenregeln)Migrate the knowledge base to the new model (plate rule + pantry + rules of thumb)
  • +
  • Rezeptbibliothek aufbauen, Start mit den 13 BeispielmahlzeitenBuild the recipe library, starting from the 13 example meals
  • +
  • Vorschlagsmaschine: „Was esse ich jetzt?" nach Uhrzeit und MahlzeitentypSuggestion engine: "What do I eat now?" by time of day and meal type
  • +
  • Teller-Builder: Lebensmittel pro Kategorie wählen, Kombination bewerten lassenPlate builder: pick foods per category, get the combination evaluated
  • +
+ +

Phase 2 — Zustand & EinkaufPhase 2 — state & shopping

+
    +
  • Umzug auf eigenes Hosting, echtes PWA-Manifest, Offline-FähigkeitMove to own hosting, real PWA manifest, offline capability
  • +
  • Einkaufshilfe mit Kategorie-Rechner und abhakbarer ListeShopping aid with category calculator and checkable list
  • +
  • Virtueller Kühlschrank mit Abbuchung nach dem KochenVirtual fridge with deduction after cooking
  • +
  • Wunschliste als Bindeglied zwischen Kühlschrank und EinkaufslisteWishlist as the link between fridge and shopping list
  • +
  • Vorschläge nach Vorrat filternFilter suggestions by available stock
  • +
  • Datenmodell auf Basis „pro 100 g" normalisieren, Mengen in den Teller-BuilderNormalise the data model to a "per 100 g" basis, add quantities to the plate builder
  • +
  • Teller-Builder-Analyse: Kalorien, Makros, Vielfalt, Zucker; Favoriten speichern → RezeptbibliothekPlate builder analysis: calories, macros, variety, sugar; save favourites → recipe library
  • +
  • UI-Politur: von funktionalem Prototyp zu hochwertig wirkender App — mit dir als UX-DesignerinUI polish: from functional prototype to an app that feels high-quality — with you leading as UX designer
  • +
+ +

Phase 3 — Agentische ModulePhase 3 — agentic modules

+
    +
  • Kühlschrank-Manager und Wochenplaner mit natürlicher SpracheFridge manager and weekly planner with natural language
  • +
  • Rezeptvorschläge, die über den kuratierten Bestand hinausgehenRecipe suggestions beyond the curated set
  • +
  • Mehrbenutzer-Betrieb mit geteiltem HaushaltMulti-user operation with a shared household
  • +
+
+ + +
+

07Offene FragenOpen questions

+

+ Zu klären, bevor die jeweilige Phase startet. + To be resolved before the respective phase starts. +

+ +
    +
  1. + Speichert localStorage im Artifact-iframe zuverlässig? Im normalen Browser bestätigt (Sprache übersteht Reload), im claude.ai-iframe selbst noch nicht getestet. App zeigt in den Settings ehrlich an, ob's geklappt hat. + Does localStorage persist reliably inside the Artifact iframe? Confirmed in a normal browser (language survives reload), not yet verified inside the claude.ai iframe itself. The app's settings screen honestly reports whether it worked. + Steffi · auf dem iPhone prüfenSteffi · verify on iPhone +
  2. +
  3. + Woher kommen neue Rezepte? Von Claude vorgeschlagen und von dir freigegeben, oder sammelst du selbst? Empfehlung: Claude schlägt vor, du kuratierst — sonst wächst die Bibliothek nicht. + Where do new recipes come from? Proposed by Claude and approved by you, or collected yourself? Recommendation: Claude proposes, you curate — otherwise the library won't grow. + Steffi +
  4. +
  5. + Wie viele Rezepte braucht ein sinnvoller Start? Vorschlag: 30–40, verteilt über die vier Tageszeiten. + How many recipes make a viable start? Proposal: 30–40, spread across the four times of day. + Steffi +
  6. +
  7. + Bekommt David eine eigene Instanz oder eine geteilte? Betrifft die Datenhaltung ab Phase 2. + Does David get his own instance or a shared one? Affects data handling from Phase 2 onwards. + Steffi +
  8. +
  9. + Wohin beim Hosting — GitHub Pages, Vercel, eigene Domain? Entscheidet über Aufwand und Sync-Möglichkeiten. + Where to host — GitHub Pages, Vercel, own domain? Determines effort and sync options. + Steffi · vor Phase 2Steffi · before Phase 2 +
  10. +
  11. + Genügen gerundete Nährwerte? Die App ist keine medizinische Anwendung; PCOS und Prädiabetes gehören ins Coaching, nicht in eine App-Logik. + Are rounded nutritional values sufficient? The app is not a medical tool; PCOS and prediabetes belong in the coaching, not in app logic. + Steffi +
  12. +
  13. + Lohnt sich eine Food-Database-API statt manueller Pflege? Open Food Facts (kostenlos, kein Key, gut für verpackte Produkte/Barcodes) vs. USDA FoodData Central (kostenlos, Key nötig, sehr genau für Grundzutaten, aber Englisch/US-zentriert) vs. Edamam (bester Rezept-Nährwert-Abgleich, aber Freemium mit Kontingent). Widerspricht evtl. dem Nicht-Ziel „kuratiert statt vollständig" — Integration würde eher die Vorratskammer ergänzen (Nährwerte nachschlagen), nicht ersetzen. + Does a food-database API pay off over manual upkeep? Open Food Facts (free, no key, good for packaged products/barcodes) vs. USDA FoodData Central (free, needs a key, very accurate for base ingredients, but English/US-centric) vs. Edamam (best recipe-nutrition matching, but freemium with quota). May cut against the non-goal "curated, not exhaustive" — integration would more likely supplement the pantry (nutrition lookup) than replace it. + Steffi · vor Phase 2Steffi · before Phase 2 +
  14. +
  15. + Wer treibt das UI-Redesign und wann im Prozess? Du bist UX-Designerin — heißt vermutlich: du lieferst Richtung/Mockups, Claude setzt um. Zeitpunkt: jetzt parallel zu Phase 1, oder erst wenn Funktionsumfang steht? + Who drives the UI redesign and at what point in the process? You're the UX designer — likely means: you supply direction/mockups, Claude implements. Timing: now, in parallel with Phase 1, or only once the feature set is settled? + Steffi +
  16. +
  17. + Teller-Builder: Was genau macht eine Kombination „gut"? Vorschlag: alle 4 Kategorien besetzt = vollständig; enthält „pick"-Markierung = Bonus; enthält „caution" = Hinweis statt Fehler (nichts ist verboten). Genaue Bewertungsstufen und Formulierung sind UX-Entscheidung. + Plate builder: what exactly makes a combination "good"? Proposal: all 4 categories filled = complete; contains a "pick" tag = bonus; contains a "caution" tag = a note, not an error (nothing is forbidden). Exact scoring tiers and wording are a UX call. + Steffi +
  18. +
  19. + Wie werden Mengen eingegeben — Stepper, Slider oder Zahlenfeld pro Chip? Vorschlag: Baukasten-Standardmenge vorausgefüllt, direkt editierbar. UX-Entscheidung. + How are quantities entered — stepper, slider, or number field per chip? Proposal: pre-filled with the building-block default, directly editable. A UX call. + Steffi +
  20. +
  21. + Vielfalt über die Woche messen — braucht Verlauf gespeicherter Teller, nicht nur den aktuellen. Erst sinnvoll, sobald Favoriten/Historie existieren (Phase 2). + Measuring variety across the week needs a history of saved plates, not just the current one. Only makes sense once favourites/history exist (Phase 2). + Steffi · vor UmsetzungSteffi · before implementation +
  22. +
  23. + Magic Button „Was esse ich jetzt?" — Konzept noch nicht ausgearbeitet. Braucht: Eingaben (Zeit, evtl. Kühlschrankinhalt, evtl. Stimmung/Zeitdruck), Ausgabelogik (ein Vorschlag vs. 3 Optionen), Verhalten bei „passt nicht" (neu würfeln? Filter?). Gehört zur Vorschlagsmaschine (Modul, Abschnitt 4), aber die Interaktion selbst ist UX-Arbeit. + Magic Button "What can I eat now?" — concept not yet worked out. Needs: inputs (time, maybe fridge contents, maybe mood/time pressure), output logic (one suggestion vs. 3 options), behaviour on "doesn't fit" (reroll? filter?). Belongs to the suggestion engine (module, section 4), but the interaction itself is UX work. + Steffi + Claude gemeinsamSteffi + Claude together +
  24. +
+ +
+ Rechtlicher HinweisLegal note + Zucker-Guide und Einkaufsguide tragen den Vermerk „Weitergabe ohne Genehmigung ist nicht gestattet". Die App baut inhaltlich auf diesem Material auf. Für den privaten Gebrauch unproblematisch — vor jeder Weitergabe, Veröffentlichung oder Nutzung durch David mit Pascal klären. Gleiches gilt für den Namen „Pocket Pascal". + The sugar guide and shopping guide carry the notice "distribution without permission is not permitted". The app builds on this material. Unproblematic for private use — clear it with Pascal before any sharing, publication, or use by David. The same applies to the name "Pocket Pascal". +
+
+ + +
+

08EntscheidungslogDecision log

+

+ Getroffene Festlegungen, damit sie nicht neu verhandelt werden. + Settled decisions, so they don't get re-litigated. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatumDateEntscheidungDecisionBegründungRationale
2026-08-03Alle Artefakte zweisprachig, DE primärAll artefacts bilingual, DE primaryDE zum eigenen Anpassen, EN als allgemeine VorlageDE for personal editing, EN as the general template
2026-08-03Buttons statt Chat als StandardButtons over chat by defaultOffene Konversation kostet im Hunger-Moment zu viel ZeitOpen conversation costs too much time in a moment of hunger
2026-08-03Später eigenes Hosting, portable Architektur ab jetztOwn hosting later, portable architecture from nowKühlschrank und AI-Module sind im Artifact nicht umsetzbarFridge and AI modules aren't feasible inside an Artifact
2026-08-03Datenmodell strukturiert statt Anzeige-StringsStructured data model instead of display stringsVoraussetzung für Übersetzung, Rechnen und FilternPrerequisite for translation, computation and filtering
2026-08-03App nur hell, Dokumente hell/dunkelApp light only, documents light/darkKüchennutzung bei Tageslicht, Druckvorlagen brauchen WeißKitchen use in daylight, print masters need white
2026-08-03Phase 1 = Knowledge Base + Rezeptbibliothek + VorschlagPhase 1 = knowledge base + recipe library + suggestionBedient Steffis Kernbedürfnis, ohne Persistenz auszukommenServes Steffi's core need without requiring persistence
2026-08-03Testen/Aktualisieren: Artifact-Link direkt auf dem iPhone öffnen, kein Neu-Hinzufügen zum Home-Bildschirm nötigTesting/updating: open the Artifact link directly on the iPhone, no need to re-add to the home screenOhne Service Worker cacht iOS nichts offline — jeder Aufruf lädt die zuletzt veröffentlichte Version live nach; das Home-Bildschirm-Icon zeigt also automatisch den neuesten StandWithout a service worker, iOS caches nothing offline — every open fetches the latest published version live, so the home-screen icon automatically shows the newest state
2026-08-03Web/PWA statt native iOS-App (kein Swift/Cursor)Web/PWA instead of a native iOS app (no Swift/Cursor)Nur 2 Nutzer (Steffi + David); bestehendes Datenmodell/Logik direkt weiterverwendbar statt Neustart der Codebasis; kein App-Store-Aufwand nötigOnly 2 users (Steffi + David); existing data model/logic carries over directly instead of a codebase restart; no App Store overhead needed
+
+
+ +
+ Pocket Pascal · Arbeitsdokument, Stand 2026-08-03 · Inhaltliche Grundlage: Ernährungsbaukasten, Einkaufsguide und Zucker-Guide von Pascal Wirth + Pocket Pascal · working document, as of 2026-08-03 · Content basis: building-block system, shopping guide and sugar guide by Pascal Wirth +
+ +
diff --git a/handover/component-library/ComponentShowcase.tsx b/handover/component-library/ComponentShowcase.tsx new file mode 100644 index 0000000..b41c19c --- /dev/null +++ b/handover/component-library/ComponentShowcase.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { GlassCard } from "./components/GlassCard"; +import { Button } from "./components/Button"; +import { BottomNav, type NavItem } from "./components/BottomNav"; +import { PlateChart } from "./components/PlateChart"; + +/** + * Demo page — every component rendered over a few different background + * gradients so the backdrop-blur actually has something to refract. + * Drop this on a route in Cursor (e.g. /showcase) to sanity-check the + * system before wiring it into real screens. + */ +export default function ComponentShowcase() { + const [dark, setDark] = useState(false); + const [active, setActive] = useState("plate"); + const [loading, setLoading] = useState(false); + + const navItems: NavItem[] = [ + { key: "plate", icon: "🍽️", label: "Teller", active: active === "plate", onClick: () => setActive("plate") }, + { key: "pantry", icon: "🧺", label: "Vorrat", active: active === "pantry", onClick: () => setActive("pantry") }, + { key: "builder", icon: "🧩", label: "Builder", active: active === "builder", onClick: () => setActive("builder") }, + { key: "settings", icon: "⚙️", label: "Mehr", active: active === "settings", onClick: () => setActive("settings") }, + ]; + + return ( +
+
+
+
+
+

+ Pocket Pascal · Component Showcase +

+

Elevated Surfaces

+
+ +
+ + {/* GlassCard variants */} +
+

+ Card +

+ +

Default

+

+ Static container — low-transparency surface, real elevation shadow, raised bevel edge. +

+
+ {}}> +

Interactive

+

Tap me — settles down 1px, shadow softens.

+
+
+ + {/* Dining Plate chart inside a GlassCard */} +
+

+ Dining Plate Chart +

+ + +
+ 🟡 Protein 30% + 🟠 Obst/Gemüse 30% + 🟨 Fett 15% + 🟢 KH 25% +
+
+
+ + {/* Buttons */} +
+

+ Buttons +

+
+ + +
+
+ + +
+
+
+ + +
+
+ ); +} diff --git a/handover/component-library/README.md b/handover/component-library/README.md new file mode 100644 index 0000000..04ba848 --- /dev/null +++ b/handover/component-library/README.md @@ -0,0 +1,58 @@ +# Pocket Pascal — Elevated Surface Component Library + +Drop this folder's contents into your Cursor React + TypeScript + Tailwind project. It's framework-agnostic (no Next.js-specific imports), works in Vite or Next. + +> **Pivoted from glassmorphism to elevation (2026-08-03).** The first pass used real see-through glass (12% white fill, 16px blur) — over a plain white/dark background it barely reads as anything, which is exactly the feedback that came back: "I don't see the glass effect." Rather than fight a page background that has nothing distinct behind it to refract, the system now uses **low-transparency solid surfaces (~90% opaque) with visible elevation shadows**, and buttons that visibly **press inward** on tap (inset shadow + 1px downward shift + slight darkening) instead of just scaling down. The `--glass-*` variable names in `theme.css` were kept to minimize the diff across components, but they now mean "surface fill / surface blur," not transparent glass — see the comment block at the top of `theme.css`. + +## Setup + +1. **Copy files** + - `styles/theme.css` → your global stylesheet, imported once in your app root (e.g. `main.tsx` or `app/layout.tsx`), **after** `@tailwind base/components/utilities`. + - `lib/cx.ts`, `components/*.tsx` → into your `src/` at matching paths, or adjust the relative imports. + - `public/fonts/*.woff2` → your project's `public/fonts/` (already the correct static latin-subset files, no need to re-fetch from Google). + - `ComponentShowcase.tsx` → mount on a scratch route to sanity-check everything renders before wiring into real screens. + +2. **Tailwind config** — enable class-based dark mode so the `.dark` class in `theme.css` works: + + ```ts + // tailwind.config.ts + export default { + darkMode: "class", + content: ["./src/**/*.{ts,tsx}"], + theme: { extend: {} }, + }; + ``` + + No plugin needed — `backdrop-filter`, `active:`, and arbitrary values (`bg-[image:var(--x)]`) are all core Tailwind v3+. This library leans on CSS variables + inline `style` for the gradient/blur values rather than baking them into the Tailwind theme, so it works the same whether or not you extend the config further. + +3. **Toggle dark mode** by adding/removing the `dark` class on `` (or any ancestor — see `ComponentShowcase.tsx` for a self-contained example that wraps in a `div` instead, for demo purposes only; in a real app put it on ``). + +## What's in here + +| File | Purpose | +|---|---| +| `styles/theme.css` | All design tokens: surface fill/blur/bevel-border, three-state shadows (resting/elevated/pressed), brand colors, surface + action gradients, `@font-face`, the `.glass-border` masked-bevel utility, PWA polish resets. | +| `components/GlassCard.tsx` | Primary container. `variant="default" \| "interactive"`. Real elevation shadow, settles down 1px on tap. | +| `components/BottomNav.tsx` | Floating pill nav, safe-area aware, gradient-on-active-icon, items press inward on tap. | +| `components/Button.tsx` | `variant="primary" \| "secondary" \| "fab"`, plus `loading`/`disabled` states. Press state = inset shadow + 1px shift + darken, not just scale. | +| `components/PlateChart.tsx` | SVG donut styled as a ceramic plate (rim + recessed well + gapped gradient slices + center label). | +| `ComponentShowcase.tsx` | Demo page rendering all of the above over the ambient page gradient, with a light/dark toggle. | + +## Deliberate deviations from the original brief — and why + +**Text color on gradient buttons/badges is `--ink` (near-black), not white.** +The brand's action gradients (lime → teal, coral → amber) are light/high-luminance by design — they're the same tokens already WCAG-audited for the rest of the Pocket Pascal system (see `/docs/CONCEPT.html`, decision log). White text on them fails AA contrast outright; a text-shadow doesn't fix a 2:1 ratio. Dark `--ink` text on these gradients measures 5.6–10.6:1 in both themes. If you introduce a new, genuinely dark gradient (e.g. for a destructive/caution CTA), white text + `text-shadow: 0 1px 2px rgba(0,0,0,0.25)` is fine there — just check the ratio first. + +**`--gradient-caution` was added** (coral → deep red) for destructive actions, since the brief's two named gradients (`primary`, `accent`) didn't cover that case and the brand already has a `--color-caution` token. + +**Fonts are loaded via local `@font-face` + static `.woff2` files, not `next/font` or a Google Fonts ``.** Keeps `theme.css` portable across Vite/Next, and avoids a runtime dependency on Google's CDN. The five files in `public/fonts/` are the exact latin-subset statics used everywhere else in this project. + +**`.glass-border` uses a masked pseudo-gradient, not `border-image`.** `border-image` ignores `border-radius` — it would square off every rounded corner. The mask-composite technique in `theme.css` respects `border-radius: inherit`, so it works on the 3xl card, the pill nav, and the 2xl buttons without extra per-component CSS. + +**Pressed state is inset-shadow + 1px shift + darken, applied via `active:` Tailwind classes referencing CSS vars (`active:shadow-[var(--shadow-pressed)]`), not JS state or `active:scale`.** Scale alone reads as "shrinking," not "pushed in" — the eye needs the shadow to flip from outside (elevated) to inside (pressed) to sell the depth illusion. `Button` and the `BottomNav` items use this; `GlassCard`'s interactive variant deliberately uses a milder version (settle 1px + soften shadow, no inset) since a card is a container you tap *into*, not a switch you press. + +## Known follow-ups (not done here) + +- `PlateChart` gap is computed from a **physical pixel gap → angular gap** at a fixed average radius; if you resize the chart a lot at runtime, re-check that 2px still reads as a visible gap at very small sizes (below ~120px it may visually disappear). +- No unit/visual regression tests included — this is a first-pass scaffold for Cursor, not a hardened library. +- `BottomNav` active-icon gradient uses `background-clip: text`, which needs `-webkit-background-clip: text` too in some older WebKit builds; not added here since Tailwind's `bg-clip-text` utility already handles the prefix. diff --git a/handover/component-library/components/BottomNav.tsx b/handover/component-library/components/BottomNav.tsx new file mode 100644 index 0000000..7240986 --- /dev/null +++ b/handover/component-library/components/BottomNav.tsx @@ -0,0 +1,88 @@ +import type { ReactNode } from "react"; +import { cx } from "../lib/cx"; + +export interface NavItem { + key: string; + icon: ReactNode; + label: string; + active?: boolean; + onClick?: () => void; + href?: string; +} + +export interface BottomNavProps { + items: NavItem[]; + className?: string; +} + +/** + * Floating Glass Pill bottom navigation. + * + * - Fixed to the bottom, centered, floats above content (not edge-to-edge). + * - Respects `env(safe-area-inset-bottom)` so it clears the iOS home indicator. + * - Active item gets the accent gradient applied directly to icon + label + * (bg-clip-text) rather than a pill behind it, so it reads against the + * glass background instead of competing with it. + */ +export function BottomNav({ items, className }: BottomNavProps) { + return ( + + ); +} diff --git a/handover/component-library/components/Button.tsx b/handover/component-library/components/Button.tsx new file mode 100644 index 0000000..55fa4c8 --- /dev/null +++ b/handover/component-library/components/Button.tsx @@ -0,0 +1,112 @@ +import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react"; +import { cx } from "../lib/cx"; + +export type ButtonVariant = "primary" | "secondary" | "fab"; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + loading?: boolean; + /** Required for `variant="fab"` — icon-only, no visible label. */ + icon?: ReactNode; +} + +function Spinner({ dark = false }: { dark?: boolean }) { + return ( + + ); +} + +/** + * Shared press behavior: 1px downward shift + swap from elevated to an + * inset shadow + slight darkening. This is what actually reads as + * "physically pushed in" — scale alone doesn't sell it, the inset shadow + * does the work. + */ +const pressable = cx( + "elevation-transition active:translate-y-px active:brightness-95", + "shadow-[var(--shadow-elevated)] active:shadow-[var(--shadow-pressed)]" +); + +const base = cx( + "relative inline-flex items-center justify-center gap-2", + "font-body font-bold no-select [-webkit-tap-highlight-color:transparent]", + "disabled:opacity-40 disabled:pointer-events-none disabled:active:translate-y-0 disabled:active:shadow-[var(--shadow-elevated)]" +); + +/** + * Touch-optimized button set. All variants meet the 48px minimum touch target. + * + * Text color rule: our brand action gradients (lime→teal, coral→amber) are + * light/high-luminance by design — see theme.css. Buttons use --ink text on + * them, not white, to stay at WCAG AA without a text-shadow crutch. The + * `secondary` variant uses --ink for the same reason (low-transparency + * surface fill, not a dark background). + */ +export const Button = forwardRef( + ({ variant = "primary", loading = false, disabled, icon, className, children, ...props }, ref) => { + const isDisabled = disabled || loading; + + if (variant === "fab") { + return ( + + ); + } + + if (variant === "secondary") { + return ( + + ); + } + + // primary + return ( + + ); + } +); +Button.displayName = "Button"; diff --git a/handover/component-library/components/GlassCard.tsx b/handover/component-library/components/GlassCard.tsx new file mode 100644 index 0000000..75afd58 --- /dev/null +++ b/handover/component-library/components/GlassCard.tsx @@ -0,0 +1,62 @@ +import { forwardRef, type HTMLAttributes } from "react"; +import { cx } from "../lib/cx"; + +export interface GlassCardProps extends HTMLAttributes { + /** + * `default` — static, sits at rest elevation. + * `interactive` — lifts on hover-capable devices, settles down 1px + shadow + * softens on press (not a full inward "pressed" look — that's reserved + * for `Button`; a card is a container you tap into, not a switch). + */ + variant?: "default" | "interactive"; +} + +/** + * Elevated Surface Card — the primary container of the design system. + * + * Low-transparency (not see-through glass): a solid-reading surface fill + * (~90% opaque) with a visible drop shadow for real elevation, plus a faint + * top-light/bottom-shadow bevel on the edge so it reads as raised off the + * page rather than flat or floating-glass. + * + * Structure (three stacked layers, all inside one rounded-3xl shell): + * 1. Base: subtle surface gradient (--surface-gradient) for texture. + * 2. Surface fill: ~90% opaque fill + 8px blur (softens whatever's behind + * it without reading as transparent) + masked bevel border. + * 3. Content: rendered above both, in normal flow. + */ +export const GlassCard = forwardRef( + ({ variant = "default", className, children, ...props }, ref) => { + const interactive = variant === "interactive"; + return ( +
+ {/* surface fill + blur */} +
+ {/* bevel border, 1px, radius-safe */} +
+ {/* content */} +
{children}
+
+ ); + } +); +GlassCard.displayName = "GlassCard"; diff --git a/handover/component-library/components/PlateChart.tsx b/handover/component-library/components/PlateChart.tsx new file mode 100644 index 0000000..598d768 --- /dev/null +++ b/handover/component-library/components/PlateChart.tsx @@ -0,0 +1,121 @@ +import { useId } from "react"; + +export interface PlateSlice { + id: string; + label: string; + value: number; + /** 2-stop gradient for this slice, e.g. ["#D9F227", "#1FD9C4"]. */ + colors: [string, string]; +} + +export interface PlateChartProps { + slices: PlateSlice[]; + size?: number; + centerLabel?: string; + centerValue?: string; + /** Physical gap between slices in px, converted to an angular gap at render time. */ + gapPx?: number; +} + +function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) { + const rad = ((angleDeg - 90) * Math.PI) / 180; + return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }; +} + +/** Donut-segment path between two radii, from startAngle to endAngle (degrees, 0 = top). */ +function arcPath(cx: number, cy: number, rOuter: number, rInner: number, startAngle: number, endAngle: number) { + const startOuter = polarToCartesian(cx, cy, rOuter, endAngle); + const endOuter = polarToCartesian(cx, cy, rOuter, startAngle); + const startInner = polarToCartesian(cx, cy, rInner, endAngle); + const endInner = polarToCartesian(cx, cy, rInner, startAngle); + const largeArc = endAngle - startAngle <= 180 ? 0 : 1; + return [ + "M", startOuter.x, startOuter.y, + "A", rOuter, rOuter, 0, largeArc, 0, endOuter.x, endOuter.y, + "L", endInner.x, endInner.y, + "A", rInner, rInner, 0, largeArc, 1, startInner.x, startInner.y, + "Z", + ].join(" "); +} + +/** + * "Dining Plate" chart — a donut styled to read as a ceramic plate: outer + * rim (gradient stroke), recessed well (radial gradient), slices as + * gradient-filled donut segments with a small gap between each, and a + * center overlay for the headline metric. + * + * Meant to live inside a `GlassCard`. All visuals are gradients — no images. + */ +export function PlateChart({ slices, size = 240, centerLabel, centerValue, gapPx = 2 }: PlateChartProps) { + const uid = useId().replace(/:/g, ""); + const cx = size / 2; + const cy = size / 2; + const rimR = size / 2 - 4; + const outerR = rimR - 14; + const innerR = size * 0.32; + + const total = slices.reduce((sum, s) => sum + s.value, 0) || 1; + const avgR = (outerR + innerR) / 2; + const gapDeg = avgR > 0 ? (gapPx / (avgR * 2 * Math.PI)) * 360 : 0; + + let angle = 0; + const segments = slices + .filter((s) => s.value > 0) + .map((slice) => { + const sweep = (slice.value / total) * 360; + const start = angle + gapDeg / 2; + const end = angle + sweep - gapDeg / 2; + angle += sweep; + return { ...slice, start, end, d: end > start ? arcPath(cx, cy, outerR, innerR, start, end) : null }; + }); + + return ( +
+ + + + + + + + + + + {segments.map((s) => ( + + + + + ))} + + + {/* ceramic rim */} + + {/* recessed well */} + + + {segments.map( + (s) => s.d && + )} + + {/* center hole, matches well so the label sits on clean ground */} + + + + {(centerLabel || centerValue) && ( +
+ {centerValue && ( + + {centerValue} + + )} + {centerLabel && ( + + {centerLabel} + + )} +
+ )} +
+ ); +} diff --git a/handover/component-library/lib/cx.ts b/handover/component-library/lib/cx.ts new file mode 100644 index 0000000..3d230a6 --- /dev/null +++ b/handover/component-library/lib/cx.ts @@ -0,0 +1,4 @@ +/** Tiny className joiner — avoids pulling in clsx/tailwind-merge as a dependency. */ +export function cx(...parts: Array): string { + return parts.filter(Boolean).join(" "); +} diff --git a/handover/component-library/public/fonts/albert-sans-400.woff2 b/handover/component-library/public/fonts/albert-sans-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0c8c5dec4da456966f7315d382de122dc0853ff0 GIT binary patch literal 10020 zcmV+4*&oF0AK6?04Br$0RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/handover/component-library/public/fonts/albert-sans-500.woff2 b/handover/component-library/public/fonts/albert-sans-500.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3327874b2532089f4544e3b665050e2827de5ca6 GIT binary patch literal 10344 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 GphW<%!~E$0 literal 0 HcmV?d00001 diff --git a/handover/component-library/public/fonts/albert-sans-700.woff2 b/handover/component-library/public/fonts/albert-sans-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b53856a3a8adc67f0bec6b9acd3e2f9541c4f553 GIT binary patch literal 10356 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 G=zjq^*5HEx literal 0 HcmV?d00001 diff --git a/handover/component-library/public/fonts/unbounded-700.woff2 b/handover/component-library/public/fonts/unbounded-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a77b17c8ebaead07f3a76a13c4ba3b30f815bd4f GIT binary patch literal 21212 zcmY(qQ+Os_7cKh6wrxA<*tTsu9ox2T+qTiMZQDu5I{kn9JZC>=-V|=utW|4_Icls~ zZt`Nx03g7>P}%|@{O{QN`tRKH|J?iE_5VMx1L$!AI&g6#CBO~%l~n{)r~x6QNRZG0 z3Eq$HFu_6qAU0r9kO%-MAy}{&L=+WHc{Rq`+EjR+3x&q=*SOZqjYKd$#NA$?IB{?c z2;?CH@5j$iaItXh#Y!arl2!z=KmrsjF=Gks3KdxB@0Ck4EKM=_lfNtxC?=>(|2kwl z&U^P#4ZJx9>5_3<%yF|_I%0WXcUUH#-3jZ0lB#lG#h3~mmFY9$pI&+ydI@ue2vg^l zD0=#`%g5YRGlDN`3yUpz4&aMb|4uZcH&frWHLN{&8YAc9Hw)O650;N<|OJ4}UL{ zA*`CmFv|IT=6j_g>Ign&`B+~1YINlj2_qOmsnIb*geCI}AyuiF(ORgfBLzi&FT>tD zy@#a`(^vrok|OSh5JC}^iVzJ4?@9U#+ESC*f>QDX4B`DMyBjtv2{La4 zV?aOmq|?Nwe2NYmTQIHys5KadT5o0O$oLb0nFxZnh8hGQ*|z-vCG@|8CK44KA;}jo zupf{v*xmPP1KI_hn%01oRW3+&dz$%SZjm`Hh#;F9hqdRy5!^YtWKnf6rRgKhIOiWS zn-vG)nJSbp2xleK6upX2=I7SYetjL7QR*647-Gm8*3$lh zj$fs0NMZsB#CYP#WKwt#vA9PWBp*M0&wX5v4KMiQTF_A<7Rl|WK3y;4KQCRg!sc@f zNKMpw6{Meg7 z70U6{rfVgDhd}l?qY;teBNQO1)L<$W0gJ_0rK&V0^UO!JUGX3I0&3L)zRx&Y1mB?_ z45WL8MCf~lB-;HX(U|yQ90G{298k1jL|<3zdkB6cg<8%WheDVVA%6Y!h&YJ#$!g4> zgH?(VgdH7~#Q@TmD_S~GC}2hcYeL4??Ko-Z0ZZ#_kmH`NXGGE~W_T6HBL;JuvbmhA zenBJw4n)*CcfnEzs%Wm0^q`%`&@`SR^)9hxDYS_cdf7$}BsQ&~VT1>(AJ~w=(fsmv z*rOPHmzCkCdptW{txc9yud*jP#UBTO17vu)Ffb*(U$2d%xQ#m8yXAeiLqUE_D=$i{ zS9Q&ras)XnU>eSi13#|_etTtC?CW(+`WYzwX(!5iK98j%ICWQ-ATLn~0&{b%jyh{* z@yJ4z%60p))WjKbz^WE>ZF(U^KP?w+M*aycFhH?fpfXpmWWjw}qN&#M>nGS%CUARZ zF%n*1C$}cF?vEYa{xBM$R-e4gFdR(-wYl9XjwO}!i7z?4AiC;S5$`L!?iixYjw_L56L#qWMT>b^^M zJ@;|sDdMKIUU+OhbnbPInPT!paS5g&?xDQd#r~e>?nc%Psk{I3T;rkUm63)&%oLJ~ z9t}>A!4GLCKix;n)ofFCz{HRF*(@kF<%rq6FeH1n`6H<&*dWVl-Hg&bpzYFwX)0W0bOs!ij zQHf_De(BQ$m+!-wS)M#NFlcBJjK9=bt$)Cd&FV+HZQVjXh4j2RTf6o*Ho&=nS0|#3 zYTpq=bnh?{8jkv;jbLb~3#>%6FT#Hz7pSua6HxMvJv{ZNeF-eS>{-Wji*nZPEZZu_ z$yJ^35Eml;OFxx`WRI?!fdPGv@aUQ8%4iXBsEZXLr8vNvqr^%`g%H^uAtHTpE~JWD z)%c_pwO@9SZDJ1cHKq-=_tXgam*jN@!lXC!;$F>hdjmmW{}~w2aW*6eVBx_EL(rw1 z^TqPV?L%NmMvi+WlBxrdBGf}>5T$8ZF!zV@EuW^@%Zt%KqWP>U{{piwCVCL%>yv*T z^{;bjr--+-<*43vF|p-}2xQsEmX7y0ZDtpV$uBv(pwJcNMR0#Zs-tk0==BhS&E+%> z-K`F0*Y)Un^fyQU__deNq=C#n!pXN5Q;wAP_@!6$G32@*p>G7v{;G)MK=48WJK6_u zmKv@=doGUJ*PRhniKJGfxo96q@pg`(0@C3#1U6?1-M>IF`Tdi1)?c#S&n# zlJyXDh`&XI{z&*#WmLdL^QIY-2HJ5Zve0aVOi(Zl7X2#J{tWChfeQ=kS6GqBm4OU7 zn;;z(Gw#`CM9Q%KEFzGkkRZeaV7r0_0M1CcA_YxqaHo*$V zDG1Gp*>oXg&nVTya7-%7iJ}r`Uk=54e_l0)u0r)~ zk!A|l1i4me2t0kvn$TiGvUZ(0ccZv#Hb$1Pvir*~d>YSMh$0PZq^8O=wnE4I46Sur zf+HLB#84zCfkgK~M=ht+z;k(tpre z2bYn^*!Iq^IBq^#qP0n7sU6Uc1Np2T;wE|ArVDVv7LtF|xRkXCuy#)|2n!BCyAY|Q zbi1E<2vz7WWYl79F9{=r-N-7g_|gXCF*qpO2i;{@rFbxb4mwUhkpZO*b@t&>-9x^# z@fTrJgcUgJv)GC1cf78(%09aBW4h(I<(t#16L^L|=n6*JXak?Gs2+?+g1g!DvlRMU z3rF?NI-LF-GuC*Rnu3NQIag=jy#J;52f~%K=Kf zTa)RJnk(0*C#Cc7dwcF9@?7XPM#SIx9`m>ugn9UVxBIL|HSKcXhbTDae%?E$aBdG% zN%+p#jl&yI5V8QM%b^IiWhsGEd^vo52HKx&mz##Cd$tS|1<|SC{Ap@t zlVLCf4oLXlh4|?9oXLf^o|6rh_EOOihjj;{NI_TW{KUyJ(Ob z36~jpBKKsU1k!=qCtLnl)vl0}D#4B(GrsThF%R0MdnT_-1C6z?SSP7f0eXza8o*yP zt|p#o)$f*}{d;Iciqb)=VP4f@?owK<#mnOA8F1ih*#PZiv5CiIQ9N>^jPn=0JiEq% zCSYHsjc>O8 zz34(3?@!5bXx7F1L>8H6p;EhP z1e3LNT^*%;ppNql4!tH|A19{puct`_aIHUYWQ0VF_8wX#q5*8}Y4E5RdW-ASwyYgE z%2XI{);YHtX?DPH~u(ivIlP>#^r)wZ889x}p#Qx{CzpnKicRG~k)i9wERI!DvFIIwD+p4O6 zJ1*Wb8pOU1*AZh9)+3QY)8!>#@Q>_l<&5mAZ2!Kh1(#AZf)0}?cdn@jUQ+-M=~k=e zVTZl^NMXRd5R6{f;+)suTsn={D04zKVxz~E_16H2rJ*0=QU@UHJF-Sm4rt7YDOT^2 zeEnJX@zNalZEN1%fvsT92~BaL80W z*mIAB=~)MSjRBgNbGPuT($TXiNH=KOl^^}iN>Y72e^;Xvae>1l4K>OO(z2mTHgT{< z!;ON~cZ}xPx6c~3en-kkc{+}3mS#YXo$Bs!& z==41%CYP+&6-w5uIMX}poAZ!Iz-x0AjxAEtpSYR;7u4^%BmGCMFq*$X7n`_0;8XZmuG-?Ue z%3{TmP=OjPW|vIKW(&hh{~#Sa=HVGbaSqN4O75~8W4no8+-W)oH;t1#Iu0RSRkaWH z)2uUQQ}zzqE;F|610&U(wzgeENP$~|zVdo1n2{i7N3k#t!GBJ*IZ$-b88S|Q-SwEb zfwLV;aVU0}_0=P&p{nzJ?Y-{G35Z67ww41BFU!|}HRk`484sCrhv?oYOACwh9X~d! z67AGkqUdC%H5RUjE8-fCQSLBdLCF2Y5*uE6Z|Z8#a> zBoUM)rlrCdf0L3lhoO}na^>arK9?gq7c&`~(zG2HlbdkS(VI|LMeGtk#pukJ5O3i0 zT1uw8Z7EcFoTR0qsK~Dvt`c%_i)q!Ob2(YYfWv0B)Y~wc0SXK{k4F+duP=%nR(yc? zgB!;on?0a;x`A=k8O|;$)j1wgu3>O>D?>GIpUx@E=_CwMJgNHtj0oxF+@2}v?` zFhXo#<*Dat`sJjsGoIk8rGI5eNs+v$UVWT2i zL2tDHRQWGmKWwS6^?&JJ;>Wsfsg^m+>{UBm!^B4+E2A$q*HL9a?RmB*U{Inzy*0KN zI|FyNck-@)T#Q`6mV-Qvj8=g3HN@h87C060U$WAWR0E`Mg~6Py_`x z$wrx6HfzenKG8SZI(?Ef!LOloT2nE+SwUJRW}B-$ba9a6KMZ zf}RvXX-ZvA4XAudHbtUEB1S?N@C5V=bPiKM888jhe1|8%9nL1@7_ra1ffib2v8rK) zxTChcJnuPC~z?WoYS`}E; z%tnG9`i~BJlHvNvk%tLM9GpDnJ?F{P zwPAQDxQwD}#W9Y_;gEDvSt!{!ovAD4BNOymA)tz@nWp%T#`mWI(se4D2$8{f2~y+; z5#naDKkN_9(S`fgp&E2STw!`}Q2Tw*3*aylnUL9shGe33DwR;X5xe z!N{ck#U%#>Nf_Z0SeY_$LO&|Jw$dBgPsY=JO0VcVBKX1oNV6xIx33|?t38ALCtj=# zcn$a}dsFA)|05Eov{c2dG2ebp&fCB!nzU@I07@h=jGzGnMod^SafqOW`T6-bZ!V*V zgEQo{#6q%DGBWl}I@ht8X>j zT-b81T+(S|l-zRk(&+Nfh2aI+nocT^a9DY$L0AL{y7=WY<*X&z|6a6rz=6@NXsGi= zSD|T}7h_Ga>@VKhk@oo`iD|Xo^^{7bO0_1Uzuq3gnz)kwRl@RR5^ew=3*iZY3d;5L zw=LOD$cfuebfU)VK4wimEyugm)kRiKdBc2{W1 zmw|T~U87117c&2sL@fu2vrSCH5wR4*(HHTekVl*FT8q{)7A%=fCKBnyYrA#&o4ty= z6pA&r@jH>PR4cFEC(%?bDx=G!xI@b;1PT-=QKfvSs)nlR6Y5tP7;}I@R3Iqf7hD!z zO2RzhXzW3eLqc}gyAX`JdD2@&(=(!nE`mV-fbe`Iq=aAGJwpc3<04ZN8p%}(6&H&Y zY|DYmhAe9y0vQ?W*1jrAWXdTOYKyfa6+=~N;4zuM;viZYe{;jRFl}9nUlyD#FUqq3 zm~#+3OvV-s>BJ8#PMCJt(U@do*4ul{WXdTR!${&OY5!tf=@|m@qRgg{k|(zOLz+7a z{x49R{{ss8e?TzODbA9pBTO(4o>G?`4GVZgwo#?8^s)et34kyIH`jmbQ-pu1LE zAX!O65P(mFf9oo6#&jT!HG8qw8Qpfqb_F-jaWZ6^ahmg(*p+Saj5*T@&#$o$qaO7C zFB2tKZVr!E&|s0_?a)$8ZNo|i%BGGc+AGZLY7T$l!btg1vfv{e9wgO=@xN$}0 z(mE?rrcGTYZ2%A86E*-cB0LGyKiWH}Ar$#pWeL}U!|`A$i~;XU+VHGpeCBrom70R_ zRF1(uFBm%_PCuI|I=C<%dXx*SheFCX8EUNM>J2Q@bMZ{nrV}SS zO$si-=AjwnhnZ)G5%FS36fN0tj58a#-nb17_cN*vak*;~%Uh%qlc(O@uO3)kl)j76 zy92!nT||}mP20939)S@6@_isH?fjrl2&eyqhPrbMvk?c85N!$J1n zqFaw2TDxR3g^V<`4Gbv*#VCgRs7xjg4MGK_(Y2SEmGn8BON}nIh5jYG3VlVC3b6zW z``>KcT0k=WDO<~5j^tObmf@5KtM*~ekRjj9_9$TxyiouM1L49Mkp2@iNzKiLkdgV2 zC!lutd-sOVhlsGaxUgN{7?BueqrvRt5~&M|m`iKLSL0EPRL2p%I8d;maT zBzC!03mkGZGCcgb^{*<;oX_t{;U(+;@u_WsRh$E~A&?Nc!Rn0K$=M7^r`(?RnD=AR zBKH5}n&pJ%V%{y!kS^K_B7z8?sn;2^IVGIhf6va#ikjiOs-S@GBuzUNC^%|t5*wI0 zev1@`V+HG+Xq9aUEOuxLXAyBni3>!|WK;tAs9GSkh9j!WaG@I{CY!lxE9fl1b^Pv_ zAt@3+dm}23(zT?YTb>PS)lXLtW<0&DfIhYVU+J+bXQetHWq^5iW# z!d5BQ)S^)yRjU&$BA=djEZLS?3SDF>WqDt7q2(r`0LENqO?@lwT;w>siaiC!q?k_d zltMxkEYL>`oA1(6Gzj~C?kU-az-6y6K*p$NT35jNc94I%7PNegp2QLQ+Za-vVctqL z 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 literal 0 HcmV?d00001 diff --git a/handover/component-library/styles/theme.css b/handover/component-library/styles/theme.css new file mode 100644 index 0000000..bc9b494 --- /dev/null +++ b/handover/component-library/styles/theme.css @@ -0,0 +1,192 @@ +/** + * Pocket Pascal — Elevated Surface Design Tokens + * + * Pivoted from heavy glassmorphism to a low-transparency, tactile system: + * cards read as solid raised surfaces (elevation shadow + faint top/bottom + * bevel), buttons visibly depress on press (inset shadow + 1px downward + * shift) instead of just scaling. `--glass-*` names kept for minimal diff + * across components — they now mean "surface fill / surface blur", not + * "see-through glass". Blur is low (8px) and opacity high (~88%): it softens + * whatever sits behind a card without reading as transparent. + * + * Toggle dark mode by adding/removing the `dark` class on + * (matches Tailwind `darkMode: 'class'` — see README). + * + * Brand hues (protein/obstgem/fett/kh + ink) come from the WCAG-audited + * Pocket Pascal palette, not generic placeholders — see /docs/CONCEPT.html + * section 05/08 for the full rationale and contrast numbers. + */ + +:root { + /* ---------- Elevated surface (formerly "glass") ---------- */ + --glass-fill: rgba(255, 255, 255, 0.9); + --glass-blur: 8px; + /* top-light / bottom-shadow bevel instead of an even glass edge — reads as raised, not see-through */ + --glass-border-from: rgba(255, 255, 255, 0.9); + --glass-border-to: rgba(0, 0, 0, 0.06); + + /* ---------- Shadows (three states: resting, elevated/lifted, pressed-in) ---------- */ + --shadow-resting: 0 1px 2px rgba(0, 0, 0, 0.06), 0 2px 6px rgba(0, 0, 0, 0.06); + --shadow-elevated: 0 4px 10px rgba(0, 0, 0, 0.10), 0 14px 28px rgba(0, 0, 0, 0.10); + --shadow-pressed: inset 0 2px 5px rgba(0, 0, 0, 0.28), inset 0 1px 1px rgba(0, 0, 0, 0.15); + /* kept for anything still referencing the old name */ + --shadow-ambient: var(--shadow-elevated); + + /* ---------- Brand ink / neutrals ---------- */ + --ink: #191800; + --ink-soft: #5c5b48; + --ink-faint: #6b6a56; + --border: #e2e8f0; + + /* ---------- Brand category colors (bright primary + dark accent pairs) ---------- */ + --color-protein: #d9f227; + --color-protein-dk: #2e3200; + --color-obstgem: #ff6a3d; + --color-obstgem-dk: #3d1508; + --color-fett: #ffb300; + --color-fett-dk: #3d2900; + --color-kh: #1fd9c4; + --color-kh-dk: #04302b; + --color-pick: #0f7a37; + --color-caution: #c81e1e; + + /* ---------- Surface gradients (subtle, sit UNDER glass layers) ---------- */ + --surface-gradient: linear-gradient(160deg, #fafaf7 0%, #f0f0ea 100%); + --page-gradient: radial-gradient(120% 100% at 0% 0%, #fff9e0 0%, #ffffff 45%), + radial-gradient(120% 100% at 100% 100%, #e2fbf6 0%, #ffffff 45%); + + /* ---------- Action gradients (vibrant, multi-stop) ---------- */ + /* NOTE: brand hues are light/high-luminance by design (see contrast audit), + so action gradients pair with --ink text, not white — this keeps every + button at AA contrast without text-shadow hacks. Do not swap to white + text on these without re-checking contrast. */ + --gradient-primary: linear-gradient(135deg, #d9f227 0%, #1fd9c4 100%); + --gradient-accent: linear-gradient(135deg, #ff6a3d 0%, #ffb300 100%); + --gradient-caution: linear-gradient(135deg, #ff6a3d 0%, #c81e1e 100%); + + /* ---------- Dining Plate chart tokens ---------- */ + --plate-rim-light: #ffffff; + --plate-rim-shadow: #d8d8d0; + --plate-well-center: #f7f7f2; + --plate-well-edge: #e6e6dd; +} + +.dark { + --glass-fill: rgba(28, 28, 21, 0.88); + --glass-blur: 8px; + --glass-border-from: rgba(255, 255, 255, 0.10); + --glass-border-to: rgba(0, 0, 0, 0.35); + + --shadow-resting: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.3); + --shadow-elevated: 0 4px 10px rgba(0, 0, 0, 0.45), 0 16px 32px rgba(0, 0, 0, 0.4); + --shadow-pressed: inset 0 2px 5px rgba(0, 0, 0, 0.6), inset 0 1px 1px rgba(0, 0, 0, 0.3); + --shadow-ambient: var(--shadow-elevated); + + --ink: #f3f2e6; + --ink-soft: #c2c1ac; + --ink-faint: #8f8e7a; + --border: #33342a; + + /* category primaries stay identical (they're the brand, not a theme) */ + --color-protein: #d9f227; + --color-protein-dk: #2e3200; + --color-obstgem: #ff6a3d; + --color-obstgem-dk: #3d1508; + --color-fett: #ffb300; + --color-fett-dk: #3d2900; + --color-kh: #1fd9c4; + --color-kh-dk: #04302b; + --color-pick: #4ade80; + --color-caution: #ff6b60; + + --surface-gradient: linear-gradient(160deg, #1c1c15 0%, #14140f 100%); + --page-gradient: radial-gradient(120% 100% at 0% 0%, #2a2a12 0%, #14140f 45%), + radial-gradient(120% 100% at 100% 100%, #0f2a26 0%, #14140f 45%); + + --gradient-primary: linear-gradient(135deg, #d9f227 0%, #1fd9c4 100%); + --gradient-accent: linear-gradient(135deg, #ff6a3d 0%, #ffb300 100%); + --gradient-caution: linear-gradient(135deg, #ff6a3d 0%, #ff6b60 100%); + + --plate-rim-light: #2a2a22; + --plate-rim-shadow: #0c0c09; + --plate-well-center: #1c1c15; + --plate-well-edge: #111109; +} + +/* ---------- Fonts ---------- */ +/* Download the two static woff2 files listed in README into /public/fonts, + then this block just works. Using local @font-face (not next/font) keeps + this file framework-agnostic for Cursor. */ +@font-face { + font-family: "Unbounded"; + font-weight: 700; + font-style: normal; + font-display: swap; + src: url("/fonts/unbounded-700.woff2") format("woff2"); +} +@font-face { + font-family: "Unbounded"; + font-weight: 900; + font-style: normal; + font-display: swap; + src: url("/fonts/unbounded-900.woff2") format("woff2"); +} +@font-face { + font-family: "Albert Sans"; + font-weight: 400; + font-style: normal; + font-display: swap; + src: url("/fonts/albert-sans-400.woff2") format("woff2"); +} +@font-face { + font-family: "Albert Sans"; + font-weight: 500; + font-style: normal; + font-display: swap; + src: url("/fonts/albert-sans-500.woff2") format("woff2"); +} +@font-face { + font-family: "Albert Sans"; + font-weight: 700; + font-style: normal; + font-display: swap; + src: url("/fonts/albert-sans-700.woff2") format("woff2"); +} + +.font-display { + font-family: "Unbounded", ui-sans-serif, sans-serif; + letter-spacing: -0.01em; +} +.font-body { + font-family: "Albert Sans", ui-sans-serif, "Segoe UI", Arial, sans-serif; +} + +/* ---------- Glass border (gradient 1px border that respects border-radius) ---------- */ +/* border-image ignores border-radius, so we use a masked pseudo-element instead. + Apply `.glass-border` to a `relative` container with `overflow-hidden` off + (the pseudo-element is inset:0 and radius:inherit). */ +.glass-border { + border-radius: inherit; + padding: 1px; + /* top-light, bottom-shadow — reads as a raised bevel, not a glass sheen */ + background: linear-gradient(180deg, var(--glass-border-from), var(--glass-border-to)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; +} + +/* Applied on top of --shadow-elevated/--shadow-resting to animate between them. */ +.elevation-transition { + transition: box-shadow 120ms ease-out, transform 120ms ease-out, filter 120ms ease-out; +} + +/* ---------- PWA polish ---------- */ +html, +body { + -webkit-tap-highlight-color: transparent; + overscroll-behavior-y: contain; +} +.no-select { + -webkit-user-select: none; + user-select: none; +} diff --git a/handover/data/foods.json b/handover/data/foods.json new file mode 100644 index 0000000..6d750f9 --- /dev/null +++ b/handover/data/foods.json @@ -0,0 +1,979 @@ +[ + { + "id": "protein", + "cls": "protein", + "ic": "🥚", + "title": { + "de": "Protein", + "en": "Protein" + }, + "thumb": { + "de": "Mindestens 15–20 g Protein pro 100 g, wenig Zucker & wenig Fett, kurze Zutatenliste.", + "en": "At least 15–20 g protein per 100 g, low sugar & low fat, short ingredient list." + }, + "items": [ + { + "id": "edamame", + "name": { + "de": "Edamame", + "en": "Edamame" + }, + "v": { + "de": "125 kcal · 14 g", + "en": "125 kcal · 14 g" + } + }, + { + "id": "egg", + "name": { + "de": "Ei (Größe M)", + "en": "Egg (size M)" + }, + "v": { + "de": "137 kcal · 13 g", + "en": "137 kcal · 13 g" + } + }, + { + "id": "pea_protein_strips", + "name": { + "de": "Erbsenproteinschnetzel", + "en": "Pea-protein strips" + }, + "v": { + "de": "367 kcal · 61 g", + "en": "367 kcal · 61 g" + } + }, + { + "id": "feta", + "name": { + "de": "Feta (leicht)", + "en": "Feta (light)" + }, + "v": { + "de": "161 kcal · 19 g", + "en": "161 kcal · 19 g" + }, + "f": "caution" + }, + { + "id": "chicken_breast", + "name": { + "de": "Hühnerbrust", + "en": "Chicken breast" + }, + "v": { + "de": "111 kcal · 24 g", + "en": "111 kcal · 24 g" + }, + "f": "pick" + }, + { + "id": "cottage_cheese", + "name": { + "de": "Hüttenkäse", + "en": "Cottage cheese" + }, + "v": { + "de": "87 kcal · 12,3 g", + "en": "87 kcal · 12.3 g" + } + }, + { + "id": "like_chicken", + "name": { + "de": "„Like Chicken\" (pflanzlich)", + "en": "\"Like Chicken\" (plant-based)" + }, + "v": { + "de": "≈140 kcal · 18 g", + "en": "≈140 kcal · 18 g" + }, + "approx": true + }, + { + "id": "low_fat_quark", + "name": { + "de": "Magerquark", + "en": "Low-fat quark" + }, + "v": { + "de": "67 kcal · 12 g", + "en": "67 kcal · 12 g" + } + }, + { + "id": "beef_fillet", + "name": { + "de": "Rinderfilet / Tatar", + "en": "Beef fillet / tartare" + }, + "v": { + "de": "112 kcal · 22 g", + "en": "112 kcal · 22 g" + }, + "f": "pick" + }, + { + "id": "silken_tofu", + "name": { + "de": "Seidentofu", + "en": "Silken tofu" + }, + "v": { + "de": "≈55 kcal · 5 g", + "en": "≈55 kcal · 5 g" + }, + "approx": true + }, + { + "id": "skyr", + "name": { + "de": "Skyr", + "en": "Skyr" + }, + "v": { + "de": "54 kcal · 10,3 g", + "en": "54 kcal · 10.3 g" + }, + "f": "pick" + }, + { + "id": "tempeh", + "name": { + "de": "Tempeh", + "en": "Tempeh" + }, + "v": { + "de": "184 kcal · 19 g", + "en": "184 kcal · 19 g" + } + }, + { + "id": "tofu", + "name": { + "de": "Tofu", + "en": "Tofu" + }, + "v": { + "de": "129 kcal · 13 g", + "en": "129 kcal · 13 g" + } + } + ], + "sub": { + "title": { + "de": "Milch & Joghurt", + "en": "Milk & yoghurt" + }, + "items": [ + { + "id": "greek_yogurt_10", + "name": { + "de": "Griech. Joghurt 10 %", + "en": "Greek yoghurt 10%" + }, + "v": { + "de": "≈115 kcal · 5 g", + "en": "≈115 kcal · 5 g" + }, + "approx": true + }, + { + "id": "natural_yogurt", + "name": { + "de": "Naturjoghurt", + "en": "Natural yoghurt" + }, + "v": { + "de": "≈61 kcal · 3,5 g", + "en": "≈61 kcal · 3.5 g" + }, + "approx": true + }, + { + "id": "oatly_barista", + "name": { + "de": "Oatly Barista", + "en": "Oatly Barista" + }, + "v": { + "de": "≈68 kcal · 1 g (kaum Protein)", + "en": "≈68 kcal · 1 g (barely protein)" + }, + "approx": true, + "f": "caution" + }, + { + "id": "whole_milk", + "name": { + "de": "Vollmilch 3,5 %", + "en": "Whole milk 3.5%" + }, + "v": { + "de": "≈64 kcal · 3,4 g", + "en": "≈64 kcal · 3.4 g" + }, + "approx": true, + "f": "caution" + } + ] + } + }, + { + "id": "snack", + "cls": "snack", + "ic": "🥤", + "title": { + "de": "Proteinsnacks für Steffi", + "en": "Protein snacks for Steffi" + }, + "thumb": { + "de": "Mindestens 15 g Protein pro Portion, unter 5 g Zucker, Fett unter 8 g — Zutatenliste beginnt nicht mit Zucker/Sirup/Schokolade.", + "en": "At least 15 g protein per serving, under 5 g sugar, fat under 8 g — ingredient list shouldn’t open with sugar/syrup/chocolate." + }, + "items": [ + { + "id": "three_eggs", + "name": { + "de": "3 Eier", + "en": "3 eggs" + }, + "v": { + "de": "≈205 kcal · 19 g", + "en": "≈205 kcal · 19 g" + }, + "approx": true + }, + { + "id": "cottage_berries", + "name": { + "de": "Hüttenkäse + Beeren", + "en": "Cottage cheese + berries" + }, + "v": { + "de": "≈210 kcal · 25 g", + "en": "≈210 kcal · 25 g" + }, + "approx": true + }, + { + "id": "chickpea_waffles", + "name": { + "de": "Kichererbsenwaffeln 90g", + "en": "Chickpea waffles 90g" + }, + "v": { + "de": "≈300 kcal · 17 g", + "en": "≈300 kcal · 17 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "pumpkin_seeds_30", + "name": { + "de": "Kürbiskerne 30g", + "en": "Pumpkin seeds 30g" + }, + "v": { + "de": "≈171 kcal · 9 g", + "en": "≈171 kcal · 9 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "lentil_waffles", + "name": { + "de": "Linsenwaffeln 90g", + "en": "Lentil waffles 90g" + }, + "v": { + "de": "≈300 kcal · 18 g", + "en": "≈300 kcal · 18 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "protein_bar", + "name": { + "de": "Magerer Proteinriegel", + "en": "Lean protein bar" + }, + "v": { + "de": "≈180 kcal · 20 g", + "en": "≈180 kcal · 20 g" + }, + "approx": true + }, + { + "id": "protein_rice_pudding", + "name": { + "de": "Proteinmilchreis (2x)", + "en": "Protein rice pudding (2x)" + }, + "v": { + "de": "≈340 kcal · 40 g", + "en": "≈340 kcal · 40 g" + }, + "approx": true + }, + { + "id": "protein_pudding", + "name": { + "de": "Proteinpudding", + "en": "Protein pudding" + }, + "v": { + "de": "≈90 kcal · 20 g", + "en": "≈90 kcal · 20 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "protein_shake", + "name": { + "de": "Proteinshake 40g", + "en": "Protein shake 40g" + }, + "v": { + "de": "≈150 kcal · 30 g", + "en": "≈150 kcal · 30 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "turkey_breast_pack", + "name": { + "de": "Putenbrust, 1 Packung", + "en": "Turkey breast, 1 pack" + }, + "v": { + "de": "≈130 kcal · 28 g", + "en": "≈130 kcal · 28 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "smoked_tofu_cubes", + "name": { + "de": "Räuchertofu-Würfel", + "en": "Smoked tofu cubes" + }, + "v": { + "de": "≈225 kcal · 25 g", + "en": "≈225 kcal · 25 g" + }, + "approx": true + }, + { + "id": "skyr_honey", + "name": { + "de": "Skyr/Griech. Joghurt 10% + Honig, 250g+", + "en": "Skyr/Greek yoghurt 10% + honey, 250g+" + }, + "v": { + "de": "≈180 kcal · 25 g", + "en": "≈180 kcal · 25 g" + }, + "approx": true + } + ], + "recipe": { + "title": { + "de": "Rezept: Whey-Waffeln", + "en": "Recipe: Whey waffles" + }, + "text": { + "de": "40 g Whey-Protein + 1 Ei + 40 g Haferflocken (fein gemahlen) + 60 ml Milch/Wasser verquirlen. ≈25 g Protein.", + "en": "Whisk 40 g whey protein + 1 egg + 40 g oats (finely ground) + 60 ml milk/water. ≈25 g protein." + } + } + }, + { + "id": "obst", + "cls": "obst", + "ic": "🍎", + "title": { + "de": "Obst", + "en": "Fruit" + }, + "thumb": { + "de": "Frisch oder tiefgekühlt ohne Zuckerzusatz, Saison bevorzugen.", + "en": "Fresh or frozen without added sugar, prefer what’s in season." + }, + "items": [ + { + "id": "apple", + "name": { + "de": "Apfel", + "en": "Apple" + }, + "v": { + "de": "Vit. C, Kalium, Kupfer", + "en": "Vit. C, potassium, copper" + } + }, + { + "id": "banana", + "name": { + "de": "Banane", + "en": "Banana" + }, + "v": { + "de": "Kalium, B6, C, Magnesium", + "en": "Potassium, B6, C, magnesium" + }, + "f": "caution" + }, + { + "id": "blueberry", + "name": { + "de": "Blaubeere", + "en": "Blueberry" + }, + "v": { + "de": "Vit. K, C, Mangan", + "en": "Vit. K, C, manganese" + }, + "f": "pick" + }, + { + "id": "strawberry", + "name": { + "de": "Erdbeere", + "en": "Strawberry" + }, + "v": { + "de": "Vit. C, Mangan, Folsäure, Kalium", + "en": "Vit. C, manganese, folate, potassium" + } + }, + { + "id": "granadilla", + "name": { + "de": "Granadilla (≈ Passionsfrucht)", + "en": "Granadilla (≈ passion fruit)" + }, + "v": { + "de": "Vit. C, A, Kalium, B2", + "en": "Vit. C, A, potassium, B2" + }, + "approx": true + }, + { + "id": "pomegranate", + "name": { + "de": "Granatapfel", + "en": "Pomegranate" + }, + "v": { + "de": "Vit. C, K, Folsäure, Kalium, B6", + "en": "Vit. C, K, folate, potassium, B6" + }, + "f": "pick" + }, + { + "id": "mango", + "name": { + "de": "Mango", + "en": "Mango" + }, + "v": { + "de": "Vit. C, A, B6, Folsäure, Kupfer", + "en": "Vit. C, A, B6, folate, copper" + }, + "f": "pick" + }, + { + "id": "nectarine", + "name": { + "de": "Nektarine", + "en": "Nectarine" + }, + "v": { + "de": "Vit. C, A, Kalium, E, Niacin", + "en": "Vit. C, A, potassium, E, niacin" + } + }, + { + "id": "orange", + "name": { + "de": "Orange", + "en": "Orange" + }, + "v": { + "de": "Vit. C, Folsäure, Kalium, Calcium", + "en": "Vit. C, folate, potassium, calcium" + } + }, + { + "id": "passion_fruit", + "name": { + "de": "Passionsfrucht", + "en": "Passion fruit" + }, + "v": { + "de": "Vit. C, A, Kalium, Eisen", + "en": "Vit. C, A, potassium, iron" + }, + "approx": true + }, + { + "id": "plum", + "name": { + "de": "Pflaume", + "en": "Plum" + }, + "v": { + "de": "Vit. C, K, Kalium, A, Kupfer", + "en": "Vit. C, K, potassium, A, copper" + } + }, + { + "id": "grapes", + "name": { + "de": "Trauben", + "en": "Grapes" + }, + "v": { + "de": "Vit. K, C, Kalium, Kupfer, Mangan", + "en": "Vit. K, C, potassium, copper, manganese" + }, + "f": "caution" + } + ] + }, + { + "id": "gemuese", + "cls": "gemuese", + "ic": "🥦", + "title": { + "de": "Gemüse", + "en": "Vegetables" + }, + "thumbLabel": { + "de": "Tipp", + "en": "Tip" + }, + "thumb": { + "de": "TK-Gemüse ist völlig okay, wenn’s schnell gehen muss — genauso nährstoffreich wie frisch, einfach ohne Salz-/Fettzusatz wählen.", + "en": "Frozen veg is perfectly fine when time is short — just as nutrient-rich as fresh, just pick it without added salt/fat." + }, + "items": [ + { + "id": "broccoli", + "name": { + "de": "Brokkoli", + "en": "Broccoli" + }, + "v": { + "de": "Vit. C, K, Folsäure, Eisen", + "en": "Vit. C, K, folate, iron" + } + }, + { + "id": "mushrooms", + "name": { + "de": "Champignons", + "en": "Mushrooms" + }, + "v": { + "de": "Kalium, Selen, Kupfer, B2", + "en": "Potassium, selenium, copper, B2" + } + }, + { + "id": "iceberg_lettuce", + "name": { + "de": "Eisbergsalat", + "en": "Iceberg lettuce" + }, + "v": { + "de": "Vit. K, A, Folsäure, Kalium — nährstoffarm", + "en": "Vit. K, A, folate, potassium — low nutrient density" + }, + "f": "caution" + }, + { + "id": "lambs_lettuce", + "name": { + "de": "Feldsalat", + "en": "Lamb’s lettuce" + }, + "v": { + "de": "Vit. A, C, Folsäure, Eisen", + "en": "Vit. A, C, folate, iron" + }, + "f": "pick" + }, + { + "id": "cucumber", + "name": { + "de": "Gurke", + "en": "Cucumber" + }, + "v": { + "de": "Vit. K, Kalium, Mangan", + "en": "Vit. K, potassium, manganese" + } + }, + { + "id": "carrot", + "name": { + "de": "Karotte", + "en": "Carrot" + }, + "v": { + "de": "Vit. A, K, Kalium, B6", + "en": "Vit. A, K, potassium, B6" + }, + "f": "pick" + }, + { + "id": "bell_pepper", + "name": { + "de": "Paprika", + "en": "Bell pepper" + }, + "v": { + "de": "Vit. C, A, B6, E", + "en": "Vit. C, A, B6, E" + } + }, + { + "id": "spinach", + "name": { + "de": "Spinat", + "en": "Spinach" + }, + "v": { + "de": "Vit. K, A, Folsäure, Eisen", + "en": "Vit. K, A, folate, iron" + }, + "f": "pick" + }, + { + "id": "tomato", + "name": { + "de": "Tomate", + "en": "Tomato" + }, + "v": { + "de": "Vit. C, K, Kalium, Folsäure", + "en": "Vit. C, K, potassium, folate" + } + }, + { + "id": "zucchini", + "name": { + "de": "Zucchini", + "en": "Zucchini" + }, + "v": { + "de": "Vit. C, Kalium, B6, Mangan", + "en": "Vit. C, potassium, B6, manganese" + } + } + ], + "note": { + "de": "Hinweis: Süßkartoffel, Kochbanane und Erbsen zählen hier als Kohlenhydratquelle, Avocado als Fettquelle — siehe dort.", + "en": "Note: sweet potato, plantain and peas count here as a carb source, avocado as a fat source — see those cards." + } + }, + { + "id": "fett", + "cls": "fett", + "ic": "🥑", + "title": { + "de": "Fett", + "en": "Fat" + }, + "thumb": { + "de": "Native/kaltgepresste Öle, ungesalzene & ungeröstete Nüsse/Kerne, Zutatenliste = nur die Zutat selbst.", + "en": "Native/cold-pressed oils, unsalted & unroasted nuts/seeds, ingredient list = just the ingredient itself." + }, + "items": [ + { + "id": "avocado", + "name": { + "de": "Avocado", + "en": "Avocado" + }, + "v": { + "de": "217 kcal · 12,5 g", + "en": "217 kcal · 12.5 g" + } + }, + { + "id": "chia_seeds", + "name": { + "de": "Chiasamen", + "en": "Chia seeds" + }, + "v": { + "de": "486 kcal · 31 g", + "en": "486 kcal · 31 g" + }, + "f": "pick" + }, + { + "id": "pumpkin_seeds", + "name": { + "de": "Kürbiskerne", + "en": "Pumpkin seeds" + }, + "v": { + "de": "570 kcal · 44 g", + "en": "570 kcal · 44 g" + } + }, + { + "id": "linseed_oil", + "name": { + "de": "Leinöl", + "en": "Linseed oil" + }, + "v": { + "de": "837 kcal · 93 g", + "en": "837 kcal · 93 g" + }, + "f": "pick" + }, + { + "id": "almonds", + "name": { + "de": "Mandeln", + "en": "Almonds" + }, + "v": { + "de": "612 kcal · 55 g", + "en": "612 kcal · 55 g" + } + }, + { + "id": "olive_oil", + "name": { + "de": "Olivenöl", + "en": "Olive oil" + }, + "v": { + "de": "857 kcal · 91,5 g", + "en": "857 kcal · 91.5 g" + } + }, + { + "id": "walnuts", + "name": { + "de": "Walnüsse", + "en": "Walnuts" + }, + "v": { + "de": "674 kcal · 62,5 g", + "en": "674 kcal · 62.5 g" + }, + "f": "pick" + } + ] + }, + { + "id": "kh", + "cls": "kh", + "ic": "🌾", + "title": { + "de": "Kohlenhydrate", + "en": "Carbohydrates" + }, + "thumb": { + "de": "Vollkorn als erste Zutat, wenig zugesetzter Zucker. Faustregel 10:1 — pro 10 g Kohlenhydrate mind. 1 g Ballaststoffe (z. B. 30 g KH → mind. 3 g Ballaststoffe).", + "en": "Whole grain as the first ingredient, low added sugar. 10:1 rule of thumb — at least 1 g fibre per 10 g carbs (e.g. 30 g carbs → at least 3 g fibre)." + }, + "items": [ + { + "id": "brown_lentils", + "name": { + "de": "Berglinsen", + "en": "Brown lentils" + }, + "v": { + "de": "319 kcal · ≈46 g", + "en": "319 kcal · ≈46 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "buckwheat", + "name": { + "de": "Buchweizen", + "en": "Buckwheat" + }, + "v": { + "de": "343 kcal · 71,5 g", + "en": "343 kcal · 71.5 g" + } + }, + { + "id": "puffed_spelt", + "name": { + "de": "Dinkel gepufft", + "en": "Puffed spelt" + }, + "v": { + "de": "≈338 kcal · 70 g", + "en": "≈338 kcal · 70 g" + }, + "approx": true + }, + { + "id": "spelt_pasta", + "name": { + "de": "Dinkel-Vollkorn-Nudeln", + "en": "Whole-grain spelt pasta" + }, + "v": { + "de": "≈338 kcal · 70 g", + "en": "≈338 kcal · 70 g" + }, + "approx": true + }, + { + "id": "peas", + "name": { + "de": "Erbsen", + "en": "Peas" + }, + "v": { + "de": "93 kcal · 10,4 g", + "en": "93 kcal · 10.4 g" + } + }, + { + "id": "oats", + "name": { + "de": "Haferflocken", + "en": "Oats" + }, + "v": { + "de": "372 kcal · 66,3 g", + "en": "372 kcal · 66.3 g" + } + }, + { + "id": "millet", + "name": { + "de": "Hirse", + "en": "Millet" + }, + "v": { + "de": "363 kcal · 69 g", + "en": "363 kcal · 69 g" + }, + "f": "caution" + }, + { + "id": "chickpeas", + "name": { + "de": "Kichererbsen", + "en": "Chickpeas" + }, + "v": { + "de": "108 kcal · 14,2 g", + "en": "108 kcal · 14.2 g" + } + }, + { + "id": "chickpea_pasta", + "name": { + "de": "Kichererbsennudeln / -couscous", + "en": "Chickpea pasta / couscous" + }, + "v": { + "de": "≈335 kcal · 50 g", + "en": "≈335 kcal · 50 g" + }, + "approx": true + }, + { + "id": "plantain", + "name": { + "de": "Kochbanane", + "en": "Plantain" + }, + "v": { + "de": "≈122 kcal · 32 g", + "en": "≈122 kcal · 32 g" + }, + "approx": true, + "f": "caution" + }, + { + "id": "lentil_pasta", + "name": { + "de": "Linsennudeln", + "en": "Lentil pasta" + }, + "v": { + "de": "≈335 kcal · 52 g", + "en": "≈335 kcal · 52 g" + }, + "approx": true + }, + { + "id": "quinoa", + "name": { + "de": "Quinoa", + "en": "Quinoa" + }, + "v": { + "de": "368 kcal · 64,2 g", + "en": "368 kcal · 64.2 g" + } + }, + { + "id": "red_lentils", + "name": { + "de": "Rote Linsen", + "en": "Red lentils" + }, + "v": { + "de": "326 kcal · ≈52 g", + "en": "326 kcal · ≈52 g" + }, + "approx": true, + "f": "pick" + }, + { + "id": "sweet_potato", + "name": { + "de": "Süßkartoffel", + "en": "Sweet potato" + }, + "v": { + "de": "86 kcal · 20,1 g", + "en": "86 kcal · 20.1 g" + }, + "f": "caution" + }, + { + "id": "french_lentils", + "name": { + "de": "Tellerlinsen", + "en": "French lentils" + }, + "v": { + "de": "234 kcal · ≈34 g", + "en": "234 kcal · ≈34 g" + }, + "approx": true, + "f": "pick" + } + ] + } +] \ No newline at end of file diff --git a/handover/data/steps.json b/handover/data/steps.json new file mode 100644 index 0000000..4342d7a --- /dev/null +++ b/handover/data/steps.json @@ -0,0 +1,66 @@ +[ + { + "n": 1, + "cls": "g", + "title": { + "de": "Protein", + "en": "Protein" + }, + "qty": { + "de": "30 g pro Mahlzeit", + "en": "30 g per meal" + }, + "text": { + "de": "Bspw. 125 g Rinderfilet, 300 g Skyr, 40 g Proteinpulver, 50 g Erbsenproteingeschnetzeltes oder 5 Eier. 150 g am Tag insgesamt.", + "en": "E.g. 125 g beef fillet, 300 g Skyr, 40 g protein powder, 50 g pea-protein strips, or 5 eggs. 150 g total per day." + } + }, + { + "n": 2, + "cls": "t", + "title": { + "de": "Obst ODER Gemüse", + "en": "Fruit OR Vegetables" + }, + "qty": { + "de": "eine Portion", + "en": "one portion" + }, + "text": { + "de": "Obst = 100 g oder eine geballte Faust. Gemüse = 200 g oder 2 Handvoll.", + "en": "Fruit = 100 g or one clenched fist. Vegetables = 200 g or 2 handfuls." + } + }, + { + "n": 3, + "cls": "o", + "title": { + "de": "Fett", + "en": "Fat" + }, + "qty": { + "de": "", + "en": "" + }, + "text": { + "de": "15 g Samen/Kerne/Nüsse (≈20 Stück, 1 EL) · ¼ Avocado · 1 EL Öl (15 g) — Nussöl außer Erdnuss, Olivenöl, Leinsamenöl.", + "en": "15 g seeds/nuts (≈20 pieces, 1 tbsp) · ¼ avocado · 1 tbsp oil (15 g) — nut oil except peanut, olive oil, linseed oil." + } + }, + { + "n": 4, + "cls": "s", + "title": { + "de": "Kohlenhydrate", + "en": "Carbohydrates" + }, + "qty": { + "de": "", + "en": "" + }, + "text": { + "de": "75 g Reis/Nudeln/Brot (trocken) · ca. 250 g rohe Kartoffeln (≈3 mittelgroße) · 50 g Haferflocken (≈5 gehäufte EL).", + "en": "75 g rice/pasta/bread (dry) · approx. 250 g raw potatoes (≈3 medium) · 50 g oats (≈5 heaped tbsp)." + } + } +] \ No newline at end of file diff --git a/handover/reference-kueche_app.html b/handover/reference-kueche_app.html new file mode 100644 index 0000000..9e34987 --- /dev/null +++ b/handover/reference-kueche_app.html @@ -0,0 +1,836 @@ + + + + + + +Pocket Pascal + + +
+ +
+ Body Comeback +

Pocket Pascal

+
+
+
+ + +
+ +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

Sprache

+
+ Deutsch / English +
+ + +
+
+
+
+
+

Weitere Einstellungen

+
+
+
+ +
+ + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20b1161..d399751 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-dom: specifier: ^19.2.8 version: 19.2.8(react@19.2.8) + react-router-dom: + specifier: ^7.18.2 + version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) shadcn: specifier: ^4.16.1 version: 4.16.1(typescript@6.0.3) @@ -63,6 +66,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.4(@types/react@19.2.18) + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 '@vitejs/plugin-react': specifier: ^6.0.4 version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.5)) @@ -1399,6 +1405,9 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -1416,6 +1425,12 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} @@ -1722,6 +1737,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} @@ -3019,6 +3038,23 @@ packages: peerDependencies: react: ^19.2.8 + react-router-dom@7.18.2: + resolution: {integrity: sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.2: + resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.8: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} @@ -3159,6 +3195,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4933,6 +4972,8 @@ snapshots: '@types/express-serve-static-core': 5.1.3 '@types/serve-static': 2.2.0 + '@types/history@4.7.11': {} + '@types/http-errors@2.0.5': {} '@types/node@24.13.3': @@ -4947,6 +4988,17 @@ snapshots: dependencies: '@types/react': 19.2.18 + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.2.18 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.2.18 + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -5258,6 +5310,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + core-js-compat@3.49.0: dependencies: browserslist: 4.28.7 @@ -6557,6 +6611,20 @@ snapshots: react: 19.2.8 scheduler: 0.27.0 + react-router-dom@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + + react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie: 1.1.1 + react: 19.2.8 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + react@19.2.8: {} readable-stream@3.6.2: @@ -6791,6 +6859,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4