From d6fceccba54831188b236598dd8f1e1d5126d0ed Mon Sep 17 00:00:00 2001 From: Paul R Kartchner Date: Sat, 1 Nov 2025 05:54:31 +0000 Subject: [PATCH] feat: add collapsible sections to cooking mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add collapsible functionality to cooking mode for better UX during cooking: - Collapsible ingredients section: Click "Ingredients" heading to hide/show full list - Collapsible instruction steps: Click step number to collapse/expand individual steps - Auto-collapse toggle: Option to automatically collapse steps when marked complete - Visual indicators: Triangle icons (▶/▼) show collapse state - Reduced opacity for collapsed steps for visual distinction This helps users focus on relevant information and reduces scrolling during cooking. Changes: - Added state management for collapsed ingredients and steps - Added toggleStepCollapse function for manual step collapse - Updated toggleStep to handle auto-collapse when enabled - Added collapsible UI with click handlers and collapse icons - Added CSS for collapsible headers and auto-collapse toggle - Styled collapsed steps with opacity and spacing adjustments 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/web/src/pages/CookingMode.tsx | 251 +++++++++++++++--------- packages/web/src/styles/CookingMode.css | 49 +++++ 2 files changed, 211 insertions(+), 89 deletions(-) diff --git a/packages/web/src/pages/CookingMode.tsx b/packages/web/src/pages/CookingMode.tsx index df06c3c..798cf5b 100644 --- a/packages/web/src/pages/CookingMode.tsx +++ b/packages/web/src/pages/CookingMode.tsx @@ -20,6 +20,9 @@ function CookingMode() { const [showInlineIngredients, setShowInlineIngredients] = useState(true); const [completedSteps, setCompletedSteps] = useState>(new Set()); const [isFullscreen, setIsFullscreen] = useState(false); + const [ingredientsCollapsed, setIngredientsCollapsed] = useState(false); + const [collapsedSteps, setCollapsedSteps] = useState>(new Set()); + const [autoCollapseOnComplete, setAutoCollapseOnComplete] = useState(false); useEffect(() => { if (id) { @@ -71,12 +74,34 @@ function CookingMode() { const newCompleted = new Set(completedSteps); if (newCompleted.has(stepNumber)) { newCompleted.delete(stepNumber); + // When unchecking, expand the step + if (autoCollapseOnComplete) { + const newCollapsed = new Set(collapsedSteps); + newCollapsed.delete(stepNumber); + setCollapsedSteps(newCollapsed); + } } else { newCompleted.add(stepNumber); + // When checking, auto-collapse if enabled + if (autoCollapseOnComplete) { + const newCollapsed = new Set(collapsedSteps); + newCollapsed.add(stepNumber); + setCollapsedSteps(newCollapsed); + } } setCompletedSteps(newCompleted); }; + const toggleStepCollapse = (stepNumber: number) => { + const newCollapsed = new Set(collapsedSteps); + if (newCollapsed.has(stepNumber)) { + newCollapsed.delete(stepNumber); + } else { + newCollapsed.add(stepNumber); + } + setCollapsedSteps(newCollapsed); + }; + const exitCookingMode = () => { if (document.fullscreenElement) { document.exitFullscreen(); @@ -219,6 +244,15 @@ function CookingMode() { Show ingredients with steps + + @@ -243,32 +277,43 @@ function CookingMode() { {/* Ingredients Section */}
-

Ingredients

+

setIngredientsCollapsed(!ingredientsCollapsed)} + className="collapsible-header" + style={{ cursor: 'pointer' }} + > + {ingredientsCollapsed ? '▶' : '▼'} + Ingredients +

- {recipe.sections && recipe.sections.length > 0 ? ( + {!ingredientsCollapsed && ( <> - {recipe.sections.map(section => ( -
-

{section.name}

- {section.timing &&

{section.timing}

} -
    - {section.ingredients?.map((ingredient) => ( -
  • - {getScaledIngredientText(ingredient)} -
  • - ))} -
-
- ))} + {recipe.sections && recipe.sections.length > 0 ? ( + <> + {recipe.sections.map(section => ( +
+

{section.name}

+ {section.timing &&

{section.timing}

} +
    + {section.ingredients?.map((ingredient) => ( +
  • + {getScaledIngredientText(ingredient)} +
  • + ))} +
+
+ ))} + + ) : ( +
    + {allIngredients.map((ingredient) => ( +
  • + {getScaledIngredientText(ingredient)} +
  • + ))} +
+ )} - ) : ( -
    - {allIngredients.map((ingredient) => ( -
  • - {getScaledIngredientText(ingredient)} -
  • - ))} -
)}
@@ -292,25 +337,101 @@ function CookingMode() { {section.timing && {section.timing}} - {sectionInstructions.map(({ instruction, matchedIngredients }) => ( -
-
- - {instruction.timing && ( - {instruction.timing} + {sectionInstructions.map(({ instruction, matchedIngredients }) => { + const isCollapsed = collapsedSteps.has(instruction.step); + return ( +
+
+ + {instruction.timing && ( + {instruction.timing} + )} +
+ + {!isCollapsed && ( + <> +
{instruction.text}
+ + {instruction.imageUrl && ( + {`Step + )} + + {showInlineIngredients && matchedIngredients.length > 0 && ( +
+ Ingredients needed: +
    + {matchedIngredients.map((ingredient) => ( +
  • + + {getScaledIngredientText(ingredient)} + +
  • + ))} +
+
+ )} + )}
+ ); + })} +
+ ); + })} + + ) : ( + <> + {instructionsWithIngredients.map(({ instruction, matchedIngredients }) => { + const isCollapsed = collapsedSteps.has(instruction.step); + return ( +
+
+ + {instruction.timing && ( + {instruction.timing} + )} +
+ {!isCollapsed && ( + <>
{instruction.text}
{instruction.imageUrl && ( @@ -335,60 +456,12 @@ function CookingMode() {
)} -
- ))} + + )} ); })} - ) : ( - <> - {instructionsWithIngredients.map(({ instruction, matchedIngredients }) => ( -
-
- - {instruction.timing && ( - {instruction.timing} - )} -
- -
{instruction.text}
- - {instruction.imageUrl && ( - {`Step - )} - - {showInlineIngredients && matchedIngredients.length > 0 && ( -
- Ingredients needed: -
    - {matchedIngredients.map((ingredient) => ( -
  • - - {getScaledIngredientText(ingredient)} - -
  • - ))} -
-
- )} -
- ))} - )} diff --git a/packages/web/src/styles/CookingMode.css b/packages/web/src/styles/CookingMode.css index 0a4d5fc..783f628 100644 --- a/packages/web/src/styles/CookingMode.css +++ b/packages/web/src/styles/CookingMode.css @@ -423,3 +423,52 @@ border: 1px solid #ccc; } } + +/* Collapsible Elements */ +.collapsible-header { + display: flex; + align-items: center; + gap: 0.5rem; + user-select: none; +} + +.collapse-icon { + display: inline-block; + font-size: 0.9em; + margin-right: 0.25rem; + transition: transform 0.2s ease; +} + +.step-number { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.instruction-step.collapsed { + opacity: 0.6; +} + +.instruction-step.collapsed .step-header { + margin-bottom: 0; +} + +/* Toggle auto-collapse checkbox */ +.toggle-auto-collapse { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + user-select: none; + background-color: white; + padding: 0.75rem 1rem; + border-radius: 8px; + border: 2px solid #e0e0e0; + font-size: 1rem; +} + +.toggle-auto-collapse input[type="checkbox"] { + width: 1.25rem; + height: 1.25rem; + cursor: pointer; +}