feat(ux/nd): collapse Find tab into level picker + two filter sections with active indicators
This commit is contained in:
parent
8c1443d156
commit
4bb93b78d1
1 changed files with 177 additions and 141 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue