feat: add collapsible sections to cooking mode
Some checks failed
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Test API Package (push) Has been cancelled
CI Pipeline / Test Web Package (push) Has been cancelled
CI Pipeline / Test Shared Package (push) Has been cancelled
CI Pipeline / Build All Packages (push) Has been cancelled
CI Pipeline / Generate Coverage Report (push) Has been cancelled
Docker Build & Deploy / Build Docker Images (push) Has been cancelled
Docker Build & Deploy / Push Docker Images (push) Has been cancelled
Docker Build & Deploy / Deploy to Staging (push) Has been cancelled
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
E2E Tests / End-to-End Tests (push) Has been cancelled
E2E Tests / E2E Tests (Mobile) (push) Has been cancelled
Security Scanning / NPM Audit (push) Has been cancelled
Security Scanning / Dependency License Check (push) Has been cancelled
Security Scanning / Code Quality Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-01 05:54:31 +00:00
parent e2c7693a8f
commit d6fceccba5
2 changed files with 211 additions and 89 deletions

View File

@@ -20,6 +20,9 @@ function CookingMode() {
const [showInlineIngredients, setShowInlineIngredients] = useState(true); const [showInlineIngredients, setShowInlineIngredients] = useState(true);
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set()); const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [ingredientsCollapsed, setIngredientsCollapsed] = useState(false);
const [collapsedSteps, setCollapsedSteps] = useState<Set<number>>(new Set());
const [autoCollapseOnComplete, setAutoCollapseOnComplete] = useState(false);
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@@ -71,12 +74,34 @@ function CookingMode() {
const newCompleted = new Set(completedSteps); const newCompleted = new Set(completedSteps);
if (newCompleted.has(stepNumber)) { if (newCompleted.has(stepNumber)) {
newCompleted.delete(stepNumber); newCompleted.delete(stepNumber);
// When unchecking, expand the step
if (autoCollapseOnComplete) {
const newCollapsed = new Set(collapsedSteps);
newCollapsed.delete(stepNumber);
setCollapsedSteps(newCollapsed);
}
} else { } else {
newCompleted.add(stepNumber); newCompleted.add(stepNumber);
// When checking, auto-collapse if enabled
if (autoCollapseOnComplete) {
const newCollapsed = new Set(collapsedSteps);
newCollapsed.add(stepNumber);
setCollapsedSteps(newCollapsed);
}
} }
setCompletedSteps(newCompleted); 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 = () => { const exitCookingMode = () => {
if (document.fullscreenElement) { if (document.fullscreenElement) {
document.exitFullscreen(); document.exitFullscreen();
@@ -219,6 +244,15 @@ function CookingMode() {
Show ingredients with steps Show ingredients with steps
</label> </label>
<label className="toggle-auto-collapse">
<input
type="checkbox"
checked={autoCollapseOnComplete}
onChange={(e) => setAutoCollapseOnComplete(e.target.checked)}
/>
Auto collapse completed steps
</label>
<button onClick={() => navigate(`/recipes/${id}/edit`)} className="manage-btn"> <button onClick={() => navigate(`/recipes/${id}/edit`)} className="manage-btn">
Edit Recipe Edit Recipe
</button> </button>
@@ -243,8 +277,17 @@ function CookingMode() {
{/* Ingredients Section */} {/* Ingredients Section */}
<div className="cooking-mode-ingredients-section"> <div className="cooking-mode-ingredients-section">
<h2>Ingredients</h2> <h2
onClick={() => setIngredientsCollapsed(!ingredientsCollapsed)}
className="collapsible-header"
style={{ cursor: 'pointer' }}
>
<span className="collapse-icon">{ingredientsCollapsed ? '▶' : '▼'}</span>
Ingredients
</h2>
{!ingredientsCollapsed && (
<>
{recipe.sections && recipe.sections.length > 0 ? ( {recipe.sections && recipe.sections.length > 0 ? (
<> <>
{recipe.sections.map(section => ( {recipe.sections.map(section => (
@@ -270,6 +313,8 @@ function CookingMode() {
))} ))}
</ul> </ul>
)} )}
</>
)}
</div> </div>
{/* Instructions Section */} {/* Instructions Section */}
@@ -292,10 +337,12 @@ function CookingMode() {
{section.timing && <span className="section-timing">{section.timing}</span>} {section.timing && <span className="section-timing">{section.timing}</span>}
</div> </div>
{sectionInstructions.map(({ instruction, matchedIngredients }) => ( {sectionInstructions.map(({ instruction, matchedIngredients }) => {
const isCollapsed = collapsedSteps.has(instruction.step);
return (
<div <div
key={instruction.id} key={instruction.id}
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''}`} className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''} ${isCollapsed ? 'collapsed' : ''}`}
> >
<div className="step-header"> <div className="step-header">
<label className="step-checkbox"> <label className="step-checkbox">
@@ -304,13 +351,22 @@ function CookingMode() {
checked={completedSteps.has(instruction.step)} checked={completedSteps.has(instruction.step)}
onChange={() => toggleStep(instruction.step)} onChange={() => toggleStep(instruction.step)}
/> />
<span className="step-number">Step {instruction.step}</span> <span
className="step-number"
onClick={() => toggleStepCollapse(instruction.step)}
style={{ cursor: 'pointer' }}
>
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
Step {instruction.step}
</span>
</label> </label>
{instruction.timing && ( {instruction.timing && (
<span className="instruction-timing">{instruction.timing}</span> <span className="instruction-timing">{instruction.timing}</span>
)} )}
</div> </div>
{!isCollapsed && (
<>
<div className="step-text">{instruction.text}</div> <div className="step-text">{instruction.text}</div>
{instruction.imageUrl && ( {instruction.imageUrl && (
@@ -335,18 +391,23 @@ function CookingMode() {
</ul> </ul>
</div> </div>
)} )}
</>
)}
</div> </div>
))} );
})}
</div> </div>
); );
})} })}
</> </>
) : ( ) : (
<> <>
{instructionsWithIngredients.map(({ instruction, matchedIngredients }) => ( {instructionsWithIngredients.map(({ instruction, matchedIngredients }) => {
const isCollapsed = collapsedSteps.has(instruction.step);
return (
<div <div
key={instruction.id} key={instruction.id}
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''}`} className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''} ${isCollapsed ? 'collapsed' : ''}`}
> >
<div className="step-header"> <div className="step-header">
<label className="step-checkbox"> <label className="step-checkbox">
@@ -355,13 +416,22 @@ function CookingMode() {
checked={completedSteps.has(instruction.step)} checked={completedSteps.has(instruction.step)}
onChange={() => toggleStep(instruction.step)} onChange={() => toggleStep(instruction.step)}
/> />
<span className="step-number">Step {instruction.step}</span> <span
className="step-number"
onClick={() => toggleStepCollapse(instruction.step)}
style={{ cursor: 'pointer' }}
>
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
Step {instruction.step}
</span>
</label> </label>
{instruction.timing && ( {instruction.timing && (
<span className="instruction-timing">{instruction.timing}</span> <span className="instruction-timing">{instruction.timing}</span>
)} )}
</div> </div>
{!isCollapsed && (
<>
<div className="step-text">{instruction.text}</div> <div className="step-text">{instruction.text}</div>
{instruction.imageUrl && ( {instruction.imageUrl && (
@@ -386,8 +456,11 @@ function CookingMode() {
</ul> </ul>
</div> </div>
)} )}
</>
)}
</div> </div>
))} );
})}
</> </>
)} )}
</div> </div>

View File

@@ -423,3 +423,52 @@
border: 1px solid #ccc; 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;
}