diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index c8b5f491f..b401bf070 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -48,6 +48,7 @@ import { PersonSimpleCircle, BookOpen, Books, + CookingPot, } from '@mana/shared-icons'; // ── Apps with entity capabilities ─────────────────────────── @@ -844,3 +845,24 @@ registerApp({ list: { load: () => import('$lib/modules/drink/ListView.svelte') }, }, }); + +registerApp({ + id: 'recipes', + name: 'Rezepte', + color: '#f97316', + icon: CookingPot, + views: { + list: { load: () => import('$lib/modules/recipes/ListView.svelte') }, + }, + contextMenuActions: [ + { + id: 'new-recipe', + label: 'Neues Rezept', + icon: Plus, + action: () => + window.dispatchEvent( + new CustomEvent('mana:quick-action', { detail: { app: 'recipes', action: 'new' } }) + ), + }, + ], +}); diff --git a/apps/mana/apps/web/src/lib/data/crypto/registry.ts b/apps/mana/apps/web/src/lib/data/crypto/registry.ts index a9811ec20..78f52ce97 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/registry.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/registry.ts @@ -410,6 +410,14 @@ export const ENCRYPTION_REGISTRY: Record = { // plaintext for indexing and daily aggregation queries. drinkEntries: { enabled: true, fields: ['name', 'note'] }, drinkPresets: { enabled: true, fields: ['name'] }, + + // ─── Recipes ───────────────────────────────────────────── + // User-typed content (title, description, ingredients list, steps) + // encrypted. `ingredients` is Ingredient[] and `steps` is string[] — + // aes.ts JSON-stringifies before wrap, same as nutriphi's `foods`. + // Plaintext (intentional): difficulty, tags, servings, times, + // isFavorite, photo refs — needed for indexing and filtering. + recipes: { enabled: true, fields: ['title', 'description', 'ingredients', 'steps'] }, }; /** diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index e91eb3a9c..b5c9f31be 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -392,6 +392,12 @@ db.version(7).stores({ drinkPresets: 'id, order, drinkType, isArchived', }); +// Schema version 8 — adds the Recipes module. +// *tags is a Dexie MultiEntry index for tag-based filtering. +db.version(8).stores({ + recipes: 'id, difficulty, isFavorite, *tags', +}); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module diff --git a/apps/mana/apps/web/src/lib/data/module-registry.ts b/apps/mana/apps/web/src/lib/data/module-registry.ts index ed236b508..e961ee232 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -89,6 +89,7 @@ import { newsModuleConfig } from '$lib/modules/news/module.config'; import { bodyModuleConfig } from '$lib/modules/body/module.config'; import { firstsModuleConfig } from '$lib/modules/firsts/module.config'; import { drinkModuleConfig } from '$lib/modules/drink/module.config'; +import { recipesModuleConfig } from '$lib/modules/recipes/module.config'; export const MODULE_CONFIGS: readonly ModuleConfig[] = [ manaCoreConfig, @@ -133,6 +134,7 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ bodyModuleConfig, firstsModuleConfig, drinkModuleConfig, + recipesModuleConfig, ]; // ─── Derived Maps ────────────────────────────────────────── diff --git a/apps/mana/apps/web/src/lib/data/seed-registry.ts b/apps/mana/apps/web/src/lib/data/seed-registry.ts index ea05c4cc0..227bee082 100644 --- a/apps/mana/apps/web/src/lib/data/seed-registry.ts +++ b/apps/mana/apps/web/src/lib/data/seed-registry.ts @@ -29,6 +29,7 @@ import { NOTES_GUEST_SEED } from '$lib/modules/notes/collections'; import { TIMES_GUEST_SEED } from '$lib/modules/times/collections'; import { PLANTS_GUEST_SEED } from '$lib/modules/plants/collections'; import { DRINK_GUEST_SEED } from '$lib/modules/drink/collections'; +import { RECIPES_GUEST_SEED } from '$lib/modules/recipes/collections'; /** * Flat list of { tableName, rows } entries. Only modules with non-empty @@ -62,6 +63,7 @@ register(NOTES_GUEST_SEED); register(TIMES_GUEST_SEED); register(PLANTS_GUEST_SEED); register(DRINK_GUEST_SEED); +register(RECIPES_GUEST_SEED); /** * Seed all module guest data into empty tables. Idempotent: tables diff --git a/apps/mana/apps/web/src/lib/modules/recipes/ListView.svelte b/apps/mana/apps/web/src/lib/modules/recipes/ListView.svelte new file mode 100644 index 000000000..91e9babd4 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/ListView.svelte @@ -0,0 +1,883 @@ + + + +
+ +
+ +
+ + {#each ['easy', 'medium', 'hard'] as const as d} + + {/each} +
+ {#if allTags.length > 0} +
+ {#each allTags as tag} + + {/each} +
+ {/if} +
+ + +
+ {#each filtered as recipe (recipe.id)} + {@const totalTime = getTotalTime(recipe)} + +
toggleExpanded(recipe.id)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleExpanded(recipe.id); + } + }} + oncontextmenu={(e) => ctxMenu.open(e, recipe)} + > +
+ {#if recipe.photoThumbnailUrl} + {recipe.title} + {:else} + {recipe.difficulty === 'easy' + ? '🥘' + : recipe.difficulty === 'medium' + ? '👨‍🍳' + : '🔥'} + {/if} + {#if recipe.isFavorite} + + {/if} +
+
+ {recipe.title} + {#if recipe.description} + {recipe.description} + {/if} +
+ + {DIFFICULTY_LABELS[recipe.difficulty].de} + + {#if totalTime}{formatTime(totalTime)}{/if} + {recipe.servings} Port. +
+ {#if recipe.tags.length > 0} +
+ {#each recipe.tags.slice(0, 3) as tag}{tag}{/each} + {#if recipe.tags.length > 3}+{recipe.tags.length - 3}{/if} +
+ {/if} +
+
+ + {#if expandedId === recipe.id} +
+ {#if recipe.ingredients.length > 0} +
+
+ Zutaten ({recipe.servings} Portionen) +
+ {#each recipe.ingredients as ing} +
+ {ing.amount} {ing.unit} + {ing.name} +
+ {/each} +
+ {/if} + {#if recipe.steps.length > 0} +
+
Zubereitung
+ {#each recipe.steps as step, i} +
+ {i + 1} + {step} +
+ {/each} +
+ {/if} +
+ {/if} + {/each} + + {#if !showCreate} + +
(showCreate = true)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + showCreate = true; + } + }} + > + + + Neues Rezept +
+ {/if} +
+ + + {#if showCreate} + +
+
Neues Rezept
+ + + + +
+ + + + +
+ +
+ Tags +
+ {#each DEFAULT_TAGS as tag} + + {/each} +
+
+ +
+ Zutaten + {#each newIngredients as ing, i} +
+ + + + {#if newIngredients.length > 1}{/if} +
+ {/each} + +
+ +
+ Schritte + {#each newSteps as _, i} +
+ {i + 1}. + + {#if newSteps.length > 1}{/if} +
+ {/each} + +
+ +
+ + +
+
+ {/if} + + {#if filtered.length === 0 && !showCreate} +
+ {#if recipes.length === 0} +

Noch keine Rezepte gespeichert.

+ + {:else} +

Keine Rezepte gefunden.

+ {/if} +
+ {/if} + + +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/recipes/collections.ts b/apps/mana/apps/web/src/lib/modules/recipes/collections.ts new file mode 100644 index 000000000..e3138ae7d --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/collections.ts @@ -0,0 +1,107 @@ +/** + * Recipes module — collection accessors and guest seed data. + */ + +import { db } from '$lib/data/database'; +import type { LocalRecipe } from './types'; + +// ─── Collection Accessors ────────────────────────────────── + +export const recipeTable = db.table('recipes'); + +// ─── Guest Seed ──────────────────────────────────────────── + +export const RECIPES_GUEST_SEED = { + recipes: [ + { + id: 'recipe-pfannkuchen', + title: 'Pfannkuchen', + description: 'Klassische deutsche Pfannkuchen — dünn und goldbraun.', + ingredients: [ + { name: 'Mehl', amount: '250', unit: 'g' }, + { name: 'Milch', amount: '400', unit: 'ml' }, + { name: 'Eier', amount: '3', unit: 'Stück' }, + { name: 'Salz', amount: '1', unit: 'Prise' }, + { name: 'Butter', amount: '2', unit: 'EL' }, + ], + steps: [ + 'Mehl, Milch, Eier und Salz zu einem glatten Teig verrühren.', + 'Teig 15 Minuten ruhen lassen.', + 'Butter in einer Pfanne erhitzen und dünne Pfannkuchen ausbacken.', + 'Von beiden Seiten goldbraun braten.', + ], + servings: 4, + prepTimeMin: 10, + cookTimeMin: 20, + difficulty: 'easy', + tags: ['Deutsch', 'Vegetarisch', 'Schnell'], + isFavorite: true, + photoMediaId: null, + photoUrl: null, + photoThumbnailUrl: null, + }, + { + id: 'recipe-aglio-olio', + title: 'Spaghetti Aglio e Olio', + description: 'Einfache italienische Pasta mit Knoblauch und Olivenöl.', + ingredients: [ + { name: 'Spaghetti', amount: '400', unit: 'g' }, + { name: 'Knoblauch', amount: '6', unit: 'Stück' }, + { name: 'Olivenöl', amount: '6', unit: 'EL' }, + { name: 'Chiliflocken', amount: '1', unit: 'TL' }, + { name: 'Petersilie', amount: '1', unit: 'Bund' }, + { name: 'Salz', amount: '1', unit: 'TL' }, + ], + steps: [ + 'Spaghetti in reichlich Salzwasser al dente kochen.', + 'Knoblauch in dünne Scheiben schneiden.', + 'Olivenöl in einer großen Pfanne erhitzen, Knoblauch und Chiliflocken bei niedriger Hitze goldbraun anbraten.', + 'Spaghetti abgießen (etwas Kochwasser aufheben) und in die Pfanne geben.', + 'Mit Kochwasser, Petersilie und Salz abschmecken.', + ], + servings: 4, + prepTimeMin: 5, + cookTimeMin: 15, + difficulty: 'easy', + tags: ['Italienisch', 'Vegetarisch', 'Schnell'], + isFavorite: false, + photoMediaId: null, + photoUrl: null, + photoThumbnailUrl: null, + }, + { + id: 'recipe-gemuese-curry', + title: 'Gemüse-Curry', + description: 'Cremiges Curry mit saisonalem Gemüse und Kokosmilch.', + ingredients: [ + { name: 'Kichererbsen (Dose)', amount: '1', unit: 'Dose' }, + { name: 'Kokosmilch', amount: '400', unit: 'ml' }, + { name: 'Süßkartoffel', amount: '1', unit: 'Stück' }, + { name: 'Spinat', amount: '200', unit: 'g' }, + { name: 'Currypaste', amount: '2', unit: 'EL' }, + { name: 'Zwiebel', amount: '1', unit: 'Stück' }, + { name: 'Knoblauch', amount: '2', unit: 'Stück' }, + { name: 'Ingwer', amount: '1', unit: 'EL' }, + { name: 'Reis', amount: '300', unit: 'g' }, + ], + steps: [ + 'Reis nach Packungsanleitung kochen.', + 'Zwiebel, Knoblauch und Ingwer fein hacken und in Öl anbraten.', + 'Currypaste hinzufügen und 1 Minute anrösten.', + 'Süßkartoffel würfeln, hinzugeben und mit Kokosmilch ablöschen.', + '15 Minuten köcheln lassen, bis die Süßkartoffel weich ist.', + 'Kichererbsen und Spinat unterrühren, 3 Minuten ziehen lassen.', + 'Mit Salz abschmecken und mit Reis servieren.', + ], + servings: 4, + prepTimeMin: 15, + cookTimeMin: 25, + difficulty: 'medium', + tags: ['Vegan', 'Asiatisch'], + isFavorite: false, + photoMediaId: null, + photoUrl: null, + photoThumbnailUrl: null, + }, + ] satisfies LocalRecipe[], +}; diff --git a/apps/mana/apps/web/src/lib/modules/recipes/index.ts b/apps/mana/apps/web/src/lib/modules/recipes/index.ts new file mode 100644 index 000000000..8bc7c5095 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/index.ts @@ -0,0 +1,25 @@ +/** + * Recipes module — barrel exports. + */ + +// ─── Stores ────────────────────────────────────────────── +export { recipesStore } from './stores/recipes.svelte'; + +// ─── Queries ───────────────────────────────────────────── +export { + useAllRecipes, + toRecipe, + filterByTag, + filterByDifficulty, + searchRecipes, + getAllTags, + getTotalTime, + formatTime, +} from './queries'; + +// ─── Collections ───────────────────────────────────────── +export { recipeTable, RECIPES_GUEST_SEED } from './collections'; + +// ─── Types ─────────────────────────────────────────────── +export { DIFFICULTY_LABELS, DIFFICULTY_COLORS, DEFAULT_TAGS, UNIT_OPTIONS } from './types'; +export type { LocalRecipe, Recipe, Ingredient, Difficulty } from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/recipes/module.config.ts b/apps/mana/apps/web/src/lib/modules/recipes/module.config.ts new file mode 100644 index 000000000..5e8081b45 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/module.config.ts @@ -0,0 +1,6 @@ +import type { ModuleConfig } from '$lib/data/module-registry'; + +export const recipesModuleConfig: ModuleConfig = { + appId: 'recipes', + tables: [{ name: 'recipes' }], +}; diff --git a/apps/mana/apps/web/src/lib/modules/recipes/queries.ts b/apps/mana/apps/web/src/lib/modules/recipes/queries.ts new file mode 100644 index 000000000..e13c33005 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/queries.ts @@ -0,0 +1,89 @@ +/** + * Reactive Queries & Pure Helpers for Recipes module. + */ + +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; +import { decryptRecords } from '$lib/data/crypto'; +import { db } from '$lib/data/database'; +import type { LocalRecipe, Recipe, Difficulty } from './types'; + +// ─── Type Converter ────────────────────────────────────── + +export function toRecipe(local: LocalRecipe): Recipe { + const now = new Date().toISOString(); + return { + id: local.id, + title: local.title, + description: local.description, + ingredients: local.ingredients ?? [], + steps: local.steps ?? [], + servings: local.servings, + prepTimeMin: local.prepTimeMin ?? null, + cookTimeMin: local.cookTimeMin ?? null, + difficulty: local.difficulty, + tags: local.tags ?? [], + isFavorite: local.isFavorite, + photoMediaId: local.photoMediaId ?? null, + photoUrl: local.photoUrl ?? null, + photoThumbnailUrl: local.photoThumbnailUrl ?? null, + createdAt: local.createdAt ?? now, + updatedAt: local.updatedAt ?? now, + }; +} + +// ─── Live Queries ───────────────────────────────────────── + +export function useAllRecipes() { + return useLiveQueryWithDefault(async () => { + const locals = await db.table('recipes').toArray(); + const visible = locals.filter((r) => !r.deletedAt); + const decrypted = await decryptRecords('recipes', visible); + return decrypted.map(toRecipe); + }, [] as Recipe[]); +} + +// ─── Pure Helpers ───────────────────────────────────────── + +/** Filter recipes by tag */ +export function filterByTag(recipes: Recipe[], tag: string): Recipe[] { + return recipes.filter((r) => r.tags.includes(tag)); +} + +/** Filter recipes by difficulty */ +export function filterByDifficulty(recipes: Recipe[], difficulty: Difficulty): Recipe[] { + return recipes.filter((r) => r.difficulty === difficulty); +} + +/** Search recipes by title or description */ +export function searchRecipes(recipes: Recipe[], query: string): Recipe[] { + const lower = query.toLowerCase(); + return recipes.filter( + (r) => + r.title.toLowerCase().includes(lower) || + r.description.toLowerCase().includes(lower) || + r.ingredients.some((i) => i.name.toLowerCase().includes(lower)) + ); +} + +/** Get all unique tags across all recipes, sorted alphabetically */ +export function getAllTags(recipes: Recipe[]): string[] { + const tags = new Set(); + for (const r of recipes) { + for (const t of r.tags) tags.add(t); + } + return [...tags].sort(); +} + +/** Get total time (prep + cook) or null if both are null */ +export function getTotalTime(recipe: Recipe): number | null { + if (recipe.prepTimeMin == null && recipe.cookTimeMin == null) return null; + return (recipe.prepTimeMin ?? 0) + (recipe.cookTimeMin ?? 0); +} + +/** Format minutes as human-readable string */ +export function formatTime(minutes: number): string { + if (minutes < 60) return `${minutes} Min.`; + const h = Math.floor(minutes / 60); + const m = minutes % 60; + return m > 0 ? `${h} Std. ${m} Min.` : `${h} Std.`; +} diff --git a/apps/mana/apps/web/src/lib/modules/recipes/stores/recipes.svelte.ts b/apps/mana/apps/web/src/lib/modules/recipes/stores/recipes.svelte.ts new file mode 100644 index 000000000..2f5a8b276 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/stores/recipes.svelte.ts @@ -0,0 +1,109 @@ +/** + * Recipes Store — Mutation-Only Service + * + * All reads are handled by liveQuery hooks in queries.ts. + */ + +import { encryptRecord } from '$lib/data/crypto'; +import { recipeTable } from '../collections'; +import { toRecipe } from '../queries'; +import type { LocalRecipe, Difficulty, Ingredient } from '../types'; + +export const recipesStore = { + async createRecipe(input: { + title: string; + description?: string; + ingredients?: Ingredient[]; + steps?: string[]; + servings?: number; + prepTimeMin?: number | null; + cookTimeMin?: number | null; + difficulty?: Difficulty; + tags?: string[]; + }) { + const newLocal: LocalRecipe = { + id: crypto.randomUUID(), + title: input.title, + description: input.description ?? '', + ingredients: input.ingredients ?? [], + steps: input.steps ?? [], + servings: input.servings ?? 4, + prepTimeMin: input.prepTimeMin ?? null, + cookTimeMin: input.cookTimeMin ?? null, + difficulty: input.difficulty ?? 'medium', + tags: input.tags ?? [], + isFavorite: false, + photoMediaId: null, + photoUrl: null, + photoThumbnailUrl: null, + }; + const snapshot = toRecipe({ ...newLocal }); + await encryptRecord('recipes', newLocal); + await recipeTable.add(newLocal); + return snapshot; + }, + + async updateRecipe( + id: string, + patch: Partial< + Pick< + LocalRecipe, + | 'title' + | 'description' + | 'ingredients' + | 'steps' + | 'servings' + | 'prepTimeMin' + | 'cookTimeMin' + | 'difficulty' + | 'tags' + | 'photoMediaId' + | 'photoUrl' + | 'photoThumbnailUrl' + > + > + ) { + const wrapped = { ...patch } as Record; + await encryptRecord('recipes', wrapped); + await recipeTable.update(id, { + ...wrapped, + updatedAt: new Date().toISOString(), + }); + }, + + async deleteRecipe(id: string) { + await recipeTable.update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async toggleFavorite(id: string) { + const existing = await recipeTable.get(id); + if (!existing) return; + await recipeTable.update(id, { + isFavorite: !existing.isFavorite, + updatedAt: new Date().toISOString(), + }); + }, + + async duplicateRecipe(id: string) { + const existing = await recipeTable.get(id); + if (!existing) return null; + + const newLocal: LocalRecipe = { + ...existing, + id: crypto.randomUUID(), + title: `Kopie von ${existing.title}`, + isFavorite: false, + createdAt: undefined, + updatedAt: undefined, + deletedAt: undefined, + }; + const snapshot = toRecipe({ ...newLocal }); + // existing record is already encrypted — re-encrypt the clone + await encryptRecord('recipes', newLocal); + await recipeTable.add(newLocal); + return snapshot; + }, +}; diff --git a/apps/mana/apps/web/src/lib/modules/recipes/types.ts b/apps/mana/apps/web/src/lib/modules/recipes/types.ts new file mode 100644 index 000000000..6b17d35ac --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/types.ts @@ -0,0 +1,99 @@ +/** + * Recipes module types. + * + * Recipe = a cooking recipe with ingredients, steps, and metadata. + */ + +import type { BaseRecord } from '@mana/local-store'; + +// ─── Subtypes ─────────────────────────────────────────── + +export type Difficulty = 'easy' | 'medium' | 'hard'; + +export interface Ingredient { + name: string; + amount: string; // string to allow "1/2", "eine Prise", etc. + unit: string; // "g", "ml", "EL", "TL", "Stück", etc. +} + +// ─── Local Record Type (Dexie) ────────────────────────── + +export interface LocalRecipe extends BaseRecord { + title: string; + description: string; + ingredients: Ingredient[]; + steps: string[]; + servings: number; + prepTimeMin: number | null; + cookTimeMin: number | null; + difficulty: Difficulty; + tags: string[]; + isFavorite: boolean; + photoMediaId: string | null; + photoUrl: string | null; + photoThumbnailUrl: string | null; +} + +// ─── Domain Type ──────────────────────────────────────── + +export interface Recipe { + id: string; + title: string; + description: string; + ingredients: Ingredient[]; + steps: string[]; + servings: number; + prepTimeMin: number | null; + cookTimeMin: number | null; + difficulty: Difficulty; + tags: string[]; + isFavorite: boolean; + photoMediaId: string | null; + photoUrl: string | null; + photoThumbnailUrl: string | null; + createdAt: string; + updatedAt: string; +} + +// ─── Constants ────────────────────────────────────────── + +export const DIFFICULTY_LABELS: Record = { + easy: { de: 'Einfach', en: 'Easy' }, + medium: { de: 'Mittel', en: 'Medium' }, + hard: { de: 'Schwer', en: 'Hard' }, +}; + +export const DIFFICULTY_COLORS: Record = { + easy: '#22c55e', + medium: '#f59e0b', + hard: '#ef4444', +}; + +export const DEFAULT_TAGS = [ + 'Vegan', + 'Vegetarisch', + 'Glutenfrei', + 'Schnell', + 'Dessert', + 'Suppe', + 'Salat', + 'Italienisch', + 'Asiatisch', + 'Deutsch', +]; + +export const UNIT_OPTIONS = [ + 'g', + 'kg', + 'ml', + 'L', + 'EL', + 'TL', + 'Stück', + 'Prise', + 'Bund', + 'Scheibe', + 'Dose', + 'Becher', + '', +]; diff --git a/apps/mana/apps/web/src/routes/(app)/recipes/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/recipes/+layout.svelte new file mode 100644 index 000000000..c214d5b55 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/recipes/+layout.svelte @@ -0,0 +1,13 @@ + + +{@render children()} diff --git a/apps/mana/apps/web/src/routes/(app)/recipes/+page.svelte b/apps/mana/apps/web/src/routes/(app)/recipes/+page.svelte new file mode 100644 index 000000000..8380101a3 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/recipes/+page.svelte @@ -0,0 +1,9 @@ + + + + Rezepte - Mana + + + diff --git a/docs/optimizable/frontend-consistency-improvements.md b/docs/optimizable/frontend-consistency-improvements.md new file mode 100644 index 000000000..9193ce6a0 --- /dev/null +++ b/docs/optimizable/frontend-consistency-improvements.md @@ -0,0 +1,51 @@ +# Frontend Consistency Improvements + +Tracked improvements for UI/styling consistency across the Mana unified app. + +## 1. Standardize ListView Styling Approach + +**Status:** Open +**Priority:** Low (no functional impact, maintenance concern) +**Effort:** Medium (13 modules to migrate) + +### Problem + +Module ListViews use two different styling approaches: + +- **Scoped CSS + `hsl(var(--color-*))` theme tokens** — 27 modules (65%) + - todo, notes, drink, contacts, journal, dreams, habits, firsts, calendar, chat, places, inventory, finance, news, body, calc, events, photos, automations, cycles, uload, picture, recipes +- **Tailwind utility classes** — 13 modules (35%) + - nutriphi, plants, moodlit, cards, presi, storage, skilltree, context, guides, memoro, who, music, playground, citycorners, questions, times + +### Why it matters + +- Tailwind modules sometimes hardcode colors (`bg-white/5`, `text-white/80`) instead of using theme tokens, breaking theme consistency. +- `transition-all` in Tailwind classes can cause rendering bugs with CSS custom properties (recipe module had invisible text until hover — fixed by switching to specific transition properties). +- Mixed approaches make it harder to audit theme compliance and onboard new contributors. + +### Recommendation + +Migrate the 13 Tailwind-based ListViews to scoped CSS with `hsl(var(--color-*))` tokens, matching the majority pattern. Key rules: + +1. Use `hsl(var(--color-foreground))`, `hsl(var(--color-muted))`, etc. — not hardcoded colors. +2. Use specific `transition: transform 0.15s, box-shadow 0.15s` — never `transition-all` (causes CSS variable animation bugs). +3. Keep scoped `