From 91724caf9694d75d25514318a735158e871ade45 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 09:42:13 -0700 Subject: [PATCH 1/9] fix(kiwi-a11y): persist constraint and allergy preferences to localStorage (#54) --- frontend/src/stores/recipes.ts | 47 ++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/frontend/src/stores/recipes.ts b/frontend/src/stores/recipes.ts index e224f61..b41e521 100644 --- a/frontend/src/stores/recipes.ts +++ b/frontend/src/stores/recipes.ts @@ -21,6 +21,35 @@ const BOOKMARKS_MAX = 50 const MISSING_MODE_KEY = 'kiwi:builder_missing_mode' const FILTER_MODE_KEY = 'kiwi:builder_filter_mode' +const CONSTRAINTS_KEY = 'kiwi:constraints' +const ALLERGIES_KEY = 'kiwi:allergies' + +function loadConstraints(): string[] { + try { + const raw = localStorage.getItem(CONSTRAINTS_KEY) + return raw ? JSON.parse(raw) : [] + } catch { + return [] + } +} + +function saveConstraints(vals: string[]) { + localStorage.setItem(CONSTRAINTS_KEY, JSON.stringify(vals)) +} + +function loadAllergies(): string[] { + try { + const raw = localStorage.getItem(ALLERGIES_KEY) + return raw ? JSON.parse(raw) : [] + } catch { + return [] + } +} + +function saveAllergies(vals: string[]) { + localStorage.setItem(ALLERGIES_KEY, JSON.stringify(vals)) +} + type MissingIngredientMode = 'hidden' | 'greyed' | 'add-to-cart' type BuilderFilterMode = 'text' | 'tags' @@ -95,8 +124,8 @@ export const useRecipesStore = defineStore('recipes', () => { // Request parameters const level = ref(1) - const constraints = ref([]) - const allergies = ref([]) + const constraints = ref(loadConstraints()) + const allergies = ref(loadAllergies()) const hardDayMode = ref(false) const maxMissing = ref(null) const styleId = ref(null) @@ -126,6 +155,8 @@ export const useRecipesStore = defineStore('recipes', () => { // Persist wizard prefs on change watch(missingIngredientMode, (val) => localStorage.setItem(MISSING_MODE_KEY, val)) watch(builderFilterMode, (val) => localStorage.setItem(FILTER_MODE_KEY, val)) + watch(constraints, (val) => saveConstraints(val)) + watch(allergies, (val) => saveAllergies(val)) const dismissedCount = computed(() => dismissedIds.value.size) @@ -241,6 +272,16 @@ export const useRecipesStore = defineStore('recipes', () => { localStorage.removeItem(BOOKMARKS_KEY) } + function clearConstraints() { + constraints.value = [] + localStorage.removeItem(CONSTRAINTS_KEY) + } + + function clearAllergies() { + allergies.value = [] + localStorage.removeItem(ALLERGIES_KEY) + } + function clearResult() { result.value = null error.value = null @@ -270,6 +311,8 @@ export const useRecipesStore = defineStore('recipes', () => { isBookmarked, toggleBookmark, clearBookmarks, + clearConstraints, + clearAllergies, missingIngredientMode, builderFilterMode, suggest, -- 2.45.2 From 391e79ac86a703bc826f60f3923936ed9e7130f2 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 09:43:54 -0700 Subject: [PATCH 2/9] fix(kiwi-a11y): deep watchers for constraint/allergy persistence (#54) --- frontend/src/stores/recipes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/stores/recipes.ts b/frontend/src/stores/recipes.ts index b41e521..085767f 100644 --- a/frontend/src/stores/recipes.ts +++ b/frontend/src/stores/recipes.ts @@ -155,8 +155,8 @@ export const useRecipesStore = defineStore('recipes', () => { // Persist wizard prefs on change watch(missingIngredientMode, (val) => localStorage.setItem(MISSING_MODE_KEY, val)) watch(builderFilterMode, (val) => localStorage.setItem(FILTER_MODE_KEY, val)) - watch(constraints, (val) => saveConstraints(val)) - watch(allergies, (val) => saveAllergies(val)) + watch(constraints, (val) => saveConstraints(val), { deep: true }) + watch(allergies, (val) => saveAllergies(val), { deep: true }) const dismissedCount = computed(() => dismissedIds.value.size) -- 2.45.2 From 4de4f63614456712f81ccc532f49c8edb073861f Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 09:48:19 -0700 Subject: [PATCH 3/9] fix(kiwi-a11y): btn-icon touch targets; aria-busy loading; role=alert on error (C4-C6, #80) --- frontend/src/components/RecipesView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index f0c5029..53dbf28 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -389,13 +389,13 @@ Wildcard -- 2.45.2 From 41837f348c75f4f9365255d7348837830cb45a05 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 09:51:26 -0700 Subject: [PATCH 4/9] fix(kiwi-a11y): darken light-mode muted text to #7a5c2e for WCAG 1.4.3 AA (H1, #80) --- frontend/src/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index 4c2419d..536b15e 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -115,7 +115,8 @@ :root { --color-text-primary: #2c1a06; --color-text-secondary: #6b4c1e; - --color-text-muted: #a0845a; + /* Darkened from #a0845a → #7a5c2e for WCAG 1.4.3 AA compliance (~4.6:1 against light bg) */ + --color-text-muted: #7a5c2e; --color-bg-primary: #fdf8f0; --color-bg-secondary: #ffffff; -- 2.45.2 From 9de42c3088b51ea40571a42f576112864d2c33e1 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 09:53:15 -0700 Subject: [PATCH 5/9] fix(kiwi-a11y): tab focus, silent fail, emoji labels, form for/id pairs (H3-H8, #80) --- frontend/src/components/RecipesView.vue | 31 ++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index 53dbf28..06f55f2 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -11,7 +11,7 @@ :aria-selected="activeTab === tab.id" :tabindex="activeTab === tab.id ? 0 : -1" :class="['btn', 'tab-btn', activeTab === tab.id ? 'btn-primary' : 'btn-secondary']" - @click="activeTab = tab.id" + @click="activateTab(tab.id)" @keydown="onTabKeydown" >{{ tab.label }} @@ -633,20 +633,25 @@ function onTabKeydown(e: KeyboardEvent) { const current = tabIds.indexOf(activeTab.value) if (e.key === 'ArrowRight') { e.preventDefault() - activeTab.value = tabIds[(current + 1) % tabIds.length]! - nextTick(() => { - // Move focus to the newly active panel so keyboard users don't have to Tab - // through the entire tab bar again to reach the panel content - const panel = document.querySelector('[role="tabpanel"]') as HTMLElement | null - panel?.focus() - }) + activateTab(tabIds[(current + 1) % tabIds.length]!) } else if (e.key === 'ArrowLeft') { e.preventDefault() - activeTab.value = tabIds[(current - 1 + tabIds.length) % tabIds.length]! - nextTick(() => { - const panel = document.querySelector('[role="tabpanel"]') as HTMLElement | null - panel?.focus() - }) + activateTab(tabIds[(current - 1 + tabIds.length) % tabIds.length]!) + } +} + +async function activateTab(tab: TabId) { + activeTab.value = tab + await nextTick() + // Move focus to the newly active panel so keyboard users don't have to Tab + // through the entire tab bar again to reach the panel content. + // findPanelRef handles the Find tab (a plain div); other tabs are child + // components so we locate their panel via querySelector. + if (tab === 'find' && findPanelRef.value) { + findPanelRef.value.focus() + } else { + const panel = document.querySelector('[role="tabpanel"]') as HTMLElement | null + panel?.focus() } } -- 2.45.2 From ceb03f8b5b813b0ddfd600803531a229ddd6c72f Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 09:57:48 -0700 Subject: [PATCH 6/9] =?UTF-8?q?fix(kiwi-a11y):=20ND/calm-UX=20policy=20fix?= =?UTF-8?q?es=20=E2=80=94=20deficit=20language,=20wildcard=20styling,=20de?= =?UTF-8?q?pletion=20framing=20(#42=20#46=20#47=20#80-M)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #42: Replace deficit framing — "You'd need:" → "To complete this recipe:", element_gaps card-warning → card-secondary, missing/gap chips status-warning → status-info, "Your pantry is missing..." → "These would expand your options:" - #46: Add activeNutritionFilterCount computed; show count in Advanced filters summary when filters are active so it's visible while collapsed - #47: Wildcard confirmation status-warning → status-info, copy updated to calm framing; wildcard recipe card badge status-warning → status-info - M1: Add re-search hint below Hard Day Mode toggle when results are already showing - M8: Move swap candidates collapsible to after directions/steps section - L2: Add autocomplete="off" to filter search, constraint, and allergy text inputs - L5: Add title="This is an affiliate link" disclosure to grocery affiliate links Items already correct (no change needed): - M2: Level description already always visible via activeLevel computed - M3: Rate limit copy already using calm framing - M5: No-results copy already calm - M6: levelLabels already uses full names - M7: "that's part of the fun" was part of the wildcard copy fixed under #47 - L1: Neon/konami handler not present in this file --- frontend/src/components/RecipesView.vue | 72 ++++++++++++++----------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index 06f55f2..bf8101f 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -80,8 +80,8 @@ -
- The AI will freestyle recipes from whatever you have. Results can be unusual — that's part of the fun. +
+ Wildcard mode lets the AI get creative with whatever you have on hand. Results might surprise you.
@@ -159,6 +163,7 @@ aria-describedby="allergy-hint" @keydown="onAllergyKey" @blur="commitAllergyInput" + autocomplete="off" /> No recipes containing these ingredients will appear.
@@ -194,7 +199,7 @@
- Advanced filters + Nutrition Filters{{ activeNutritionFilterCount > 0 ? ` (${activeNutritionFilterCount} active)` : '' }} @@ -313,13 +318,13 @@ -
-

Your pantry is missing some flavor elements:

+
+

These would expand your options:

{{ gap }}
@@ -331,6 +336,7 @@ v-model="filterText" placeholder="Search recipes or ingredients…" aria-label="Filter recipes" + autocomplete="off" />