feat(ux/nd): collapse Find tab into level picker + two filter sections with active indicators

This commit is contained in:
pyr0ball 2026-04-08 22:51:43 -07:00
parent 8c1443d156
commit 4bb93b78d1

View file

@ -66,6 +66,14 @@
</label> </label>
</div> </div>
<!-- Dietary Preferences (collapsible) -->
<details class="collapsible form-group">
<summary class="collapsible-summary filter-summary">
Dietary preferences
<span v-if="dietaryActive" class="filter-active-dot" aria-label="filters active"></span>
</summary>
<div class="collapsible-body">
<!-- Dietary Constraints Tags --> <!-- Dietary Constraints Tags -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Dietary Constraints</label> <label class="form-label">Dietary Constraints</label>
@ -125,7 +133,7 @@
</p> </p>
</div> </div>
<!-- Shopping Mode --> <!-- Shopping Mode (temporary home moves to Shopping tab in #71) -->
<div class="form-group"> <div class="form-group">
<label class="flex-start gap-sm shopping-toggle"> <label class="flex-start gap-sm shopping-toggle">
<input type="checkbox" v-model="recipesStore.shoppingMode" /> <input type="checkbox" v-model="recipesStore.shoppingMode" />
@ -136,7 +144,7 @@
</p> </p>
</div> </div>
<!-- Max Missing hidden in shopping mode (it's lifted automatically) --> <!-- Max Missing hidden in shopping mode -->
<div v-if="!recipesStore.shoppingMode" class="form-group"> <div v-if="!recipesStore.shoppingMode" class="form-group">
<label class="form-label">Max Missing Ingredients (optional)</label> <label class="form-label">Max Missing Ingredients (optional)</label>
<input <input
@ -149,66 +157,54 @@
@input="onMaxMissingInput" @input="onMaxMissingInput"
/> />
</div> </div>
</div>
</details>
<!-- Nutrition Filters --> <!-- Advanced Filters (collapsible) -->
<details class="collapsible form-group"> <details class="collapsible form-group">
<summary class="form-label collapsible-summary nutrition-summary"> <summary class="collapsible-summary filter-summary">
Nutrition Filters <span class="text-muted text-xs">(per recipe, optional)</span> Advanced filters
<span v-if="advancedActive" class="filter-active-dot" aria-label="filters active"></span>
</summary> </summary>
<div class="collapsible-body">
<!-- Nutrition Filters -->
<div class="form-group">
<label class="form-label">Nutrition limits <span class="text-muted text-xs">(per recipe, optional)</span></label>
<div class="nutrition-filters-grid mt-xs"> <div class="nutrition-filters-grid mt-xs">
<div class="form-group"> <div class="form-group">
<label class="form-label">Max Calories</label> <label class="form-label">Max Calories</label>
<input <input type="number" class="form-input" min="0" placeholder="e.g. 600"
type="number"
class="form-input"
min="0"
placeholder="e.g. 600"
:value="recipesStore.nutritionFilters.max_calories ?? ''" :value="recipesStore.nutritionFilters.max_calories ?? ''"
@input="onNutritionInput('max_calories', $event)" @input="onNutritionInput('max_calories', $event)" />
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Max Sugar (g)</label> <label class="form-label">Max Sugar (g)</label>
<input <input type="number" class="form-input" min="0" placeholder="e.g. 10"
type="number"
class="form-input"
min="0"
placeholder="e.g. 10"
:value="recipesStore.nutritionFilters.max_sugar_g ?? ''" :value="recipesStore.nutritionFilters.max_sugar_g ?? ''"
@input="onNutritionInput('max_sugar_g', $event)" @input="onNutritionInput('max_sugar_g', $event)" />
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Max Carbs (g)</label> <label class="form-label">Max Carbs (g)</label>
<input <input type="number" class="form-input" min="0" placeholder="e.g. 50"
type="number"
class="form-input"
min="0"
placeholder="e.g. 50"
:value="recipesStore.nutritionFilters.max_carbs_g ?? ''" :value="recipesStore.nutritionFilters.max_carbs_g ?? ''"
@input="onNutritionInput('max_carbs_g', $event)" @input="onNutritionInput('max_carbs_g', $event)" />
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Max Sodium (mg)</label> <label class="form-label">Max Sodium (mg)</label>
<input <input type="number" class="form-input" min="0" placeholder="e.g. 800"
type="number"
class="form-input"
min="0"
placeholder="e.g. 800"
:value="recipesStore.nutritionFilters.max_sodium_mg ?? ''" :value="recipesStore.nutritionFilters.max_sodium_mg ?? ''"
@input="onNutritionInput('max_sodium_mg', $event)" @input="onNutritionInput('max_sodium_mg', $event)" />
/>
</div> </div>
</div> </div>
<p class="text-xs text-muted mt-xs"> <p class="text-xs text-muted mt-xs">
Recipes without nutrition data always appear. Filters apply to food.com and estimated values. Recipes without nutrition data always appear.
</p> </p>
</details> </div>
<!-- Cuisine Style (Level 3+ only) --> <!-- Cuisine Style (Level 3+ only) -->
<div v-if="recipesStore.level >= 3" class="form-group"> <div v-if="recipesStore.level >= 3" class="form-group">
<label class="form-label">Cuisine Style <span class="text-muted text-xs">(Level 3+)</span></label> <label class="form-label">Cuisine Style</label>
<div class="flex flex-wrap gap-xs"> <div class="flex flex-wrap gap-xs">
<button <button
v-for="style in cuisineStyles" v-for="style in cuisineStyles"
@ -230,6 +226,8 @@
@keydown.enter="recipesStore.category = categoryInput.trim() || null" @keydown.enter="recipesStore.category = categoryInput.trim() || null"
/> />
</div> </div>
</div>
</details>
<!-- Suggest Button --> <!-- Suggest Button -->
<div class="suggest-row"> <div class="suggest-row">
@ -657,6 +655,20 @@ const levels = [
{ value: 4, label: 'Surprise Me 🎲', description: 'Fully AI-generated — open-ended and occasionally unexpected. Requires paid tier.' }, { value: 4, label: 'Surprise Me 🎲', description: 'Fully AI-generated — open-ended and occasionally unexpected. Requires paid tier.' },
] ]
const dietaryActive = computed(() =>
recipesStore.constraints.length > 0 ||
recipesStore.allergies.length > 0 ||
recipesStore.hardDayMode ||
recipesStore.shoppingMode
)
const advancedActive = computed(() =>
Object.values(recipesStore.nutritionFilters).some((v) => v !== null) ||
recipesStore.maxMissing !== null ||
!!recipesStore.category ||
!!recipesStore.styleId
)
const activeLevel = computed(() => levels.find(l => l.value === recipesStore.level)) const activeLevel = computed(() => levels.find(l => l.value === recipesStore.level))
const cuisineStyles = [ const cuisineStyles = [
@ -1052,6 +1064,30 @@ details[open] .collapsible-summary::before {
content: '▼ '; content: '▼ ';
} }
.filter-summary {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-weight: 500;
color: var(--color-text-primary);
font-size: var(--font-size-sm);
}
.filter-active-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--color-primary);
flex-shrink: 0;
}
.collapsible-body {
padding-top: var(--spacing-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.swap-row { .swap-row {
padding: var(--spacing-xs) 0; padding: var(--spacing-xs) 0;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);