diff --git a/frontend/src/components/InventoryList.vue b/frontend/src/components/InventoryList.vue index 9fe8771..64ee162 100644 --- a/frontend/src/components/InventoryList.vue +++ b/frontend/src/components/InventoryList.vue @@ -105,9 +105,9 @@
- - - + + +
@@ -160,9 +160,9 @@
- - - + + +
@@ -209,9 +209,9 @@
- - - + + +
@@ -765,7 +765,8 @@ function getItemClass(item: InventoryItem): string { flex-direction: column; gap: var(--spacing-md); padding: var(--spacing-xs) 0 0; - overflow-x: hidden; /* prevent item rows from expanding page width on mobile */ + overflow-x: hidden; + width: 100%; /* Firefox: explicit width stops flex column from auto-sizing to content */ } /* ============================================ @@ -989,6 +990,9 @@ function getItemClass(item: InventoryItem): string { display: flex; flex-direction: column; gap: var(--spacing-sm); + width: 100%; + max-width: 100%; + overflow-x: hidden; } .inventory-header { @@ -1016,6 +1020,8 @@ function getItemClass(item: InventoryItem): string { border: 1px solid var(--color-border); border-radius: var(--radius-lg); overflow: hidden; + width: 100%; + max-width: 100%; } .inv-row { @@ -1024,6 +1030,8 @@ function getItemClass(item: InventoryItem): string { gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); border-left: 3px solid var(--color-border); + max-width: 100%; + box-sizing: border-box; background: var(--color-bg-card); transition: background 0.15s ease; min-height: 52px; diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index b3f5676..b80578d 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -1,5 +1,39 @@ @@ -420,17 +538,111 @@ import { ref, computed, onMounted } from 'vue' import { useRecipesStore } from '../stores/recipes' import { useInventoryStore } from '../stores/inventory' +import RecipeDetailPanel from './RecipeDetailPanel.vue' +import RecipeBrowserPanel from './RecipeBrowserPanel.vue' +import SavedRecipesPanel from './SavedRecipesPanel.vue' import type { RecipeSuggestion, GroceryLink } from '../services/api' +import { recipesAPI } from '../services/api' const recipesStore = useRecipesStore() const inventoryStore = useInventoryStore() +// Tab state +type TabId = 'find' | 'browse' | 'saved' +const tabs: Array<{ id: TabId; label: string }> = [ + { id: 'find', label: 'Find' }, + { id: 'browse', label: 'Browse' }, + { id: 'saved', label: 'Saved' }, +] +const activeTab = ref('find') + +function onTabKeydown(e: KeyboardEvent) { + const tabIds: TabId[] = ['find', 'browse', 'saved'] + const current = tabIds.indexOf(activeTab.value) + if (e.key === 'ArrowRight') { + e.preventDefault() + activeTab.value = tabIds[(current + 1) % tabIds.length]! + } else if (e.key === 'ArrowLeft') { + e.preventDefault() + activeTab.value = tabIds[(current - 1 + tabIds.length) % tabIds.length]! + } +} + +// Browser/saved tab recipe detail panel (fetches full recipe from API) +const browserSelectedRecipe = ref(null) + +async function openRecipeById(recipeId: number) { + try { + browserSelectedRecipe.value = await recipesAPI.getRecipe(recipeId) + } catch { + // silently ignore — recipe may not exist + } +} + // Local input state for tags const constraintInput = ref('') const allergyInput = ref('') const categoryInput = ref('') const isLoadingMore = ref(false) +// Recipe detail panel (Find tab) +const selectedRecipe = ref(null) + +// Filter state (#21) +const filterText = ref('') +const filterLevel = ref(null) +const filterMissing = ref(null) + +const availableLevels = computed(() => { + if (!recipesStore.result) return [] + return [...new Set(recipesStore.result.suggestions.map((r) => r.level))].sort() +}) + +const filteredSuggestions = computed(() => { + if (!recipesStore.result) return [] + let items = recipesStore.result.suggestions + const q = filterText.value.trim().toLowerCase() + if (q) { + items = items.filter((r) => + r.title.toLowerCase().includes(q) || + r.matched_ingredients.some((i) => i.toLowerCase().includes(q)) || + r.missing_ingredients.some((i) => i.toLowerCase().includes(q)) + ) + } + if (filterLevel.value !== null) { + items = items.filter((r) => r.level === filterLevel.value) + } + if (filterMissing.value !== null) { + items = items.filter((r) => r.missing_ingredients.length <= filterMissing.value!) + } + return items +}) + +const hasActiveFilters = computed( + () => filterText.value.trim() !== '' || filterLevel.value !== null || filterMissing.value !== null +) + +function clearFilters() { + filterText.value = '' + filterLevel.value = null + filterMissing.value = null +} + +const selectedGroceryLinks = computed(() => { + if (!selectedRecipe.value || !recipesStore.result) return [] + const missing = new Set(selectedRecipe.value.missing_ingredients.map((s) => s.toLowerCase())) + return recipesStore.result.grocery_links.filter((l) => missing.has(l.ingredient.toLowerCase())) +}) + +function openRecipe(recipe: RecipeSuggestion) { + selectedRecipe.value = recipe +} + +function onCooked(recipe: RecipeSuggestion) { + recipesStore.logCook(recipe.id, recipe.title) + recipesStore.dismiss(recipe.id) +} + const levels = [ { value: 1, label: '1 — From Pantry' }, { value: 2, label: '2 — Creative Swaps' }, @@ -554,6 +766,16 @@ onMounted(async () => {