From e1aaa241869116182215a3fb489cf51bd0b77113 Mon Sep 17 00:00:00 2001 From: Andreas Weyer Date: Fri, 16 Jan 2026 13:26:30 +0000 Subject: [PATCH] Add collapsible-card-header component to consolidate and improve folding --- src/components/day-schedule.tsx | 181 +++++++++--------- src/components/settings.tsx | 97 +++++++--- src/components/ui/collapsible-card-header.tsx | 59 ++++++ 3 files changed, 219 insertions(+), 118 deletions(-) create mode 100644 src/components/ui/collapsible-card-header.tsx diff --git a/src/components/day-schedule.tsx b/src/components/day-schedule.tsx index 9178c94..5b94897 100644 --- a/src/components/day-schedule.tsx +++ b/src/components/day-schedule.tsx @@ -10,12 +10,13 @@ import React from 'react'; import { Button } from './ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; +import { Card, CardContent } from './ui/card'; import { Badge } from './ui/badge'; import { FormTimeInput } from './ui/form-time-input'; import { FormNumericInput } from './ui/form-numeric-input'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; -import { Plus, Copy, Trash2, ArrowDownAZ, ChevronDown, ChevronUp, TrendingUp, TrendingDown } from 'lucide-react'; +import CollapsibleCardHeader from './ui/collapsible-card-header'; +import { Plus, Copy, Trash2, ArrowDownAZ, TrendingUp, TrendingDown } from 'lucide-react'; import type { DayGroup } from '../constants/defaults'; interface DayScheduleProps { @@ -46,6 +47,23 @@ const DaySchedule: React.FC = ({ // Track collapsed state for each day (by day ID) const [collapsedDays, setCollapsedDays] = React.useState>(new Set()); + // Load and persist collapsed days state + React.useEffect(() => { + const savedCollapsed = localStorage.getItem('dayScheduleCollapsedDays_v1'); + if (savedCollapsed) { + try { + const collapsedArray = JSON.parse(savedCollapsed); + setCollapsedDays(new Set(collapsedArray)); + } catch (e) { + console.warn('Failed to load collapsed days state:', e); + } + } + }, []); + + const saveCollapsedDays = (newCollapsedDays: Set) => { + localStorage.setItem('dayScheduleCollapsedDays_v1', JSON.stringify(Array.from(newCollapsedDays))); + }; + const toggleDayCollapse = (dayId: string) => { setCollapsedDays(prev => { const newSet = new Set(prev); @@ -54,6 +72,7 @@ const DaySchedule: React.FC = ({ } else { newSet.add(dayId); } + saveCollapsedDays(newSet); return newSet; }); }; @@ -89,89 +108,13 @@ const DaySchedule: React.FC = ({ return ( - -
-
- - - {day.isTemplate ? t('regularPlan') : t('alternativePlan')} - - - {t('day')} {dayIndex + 1} - - {!day.isTemplate && doseCountDiff !== 0 ? ( - - - - - - -

- {doseCountDiff > 0 ? '+' : ''}{doseCountDiff} {Math.abs(doseCountDiff) === 1 ? t('dose') : t('doses')} {t('comparedToRegularPlan')} -

-
-
-
- ) : ( - - {day.doses.length} {day.doses.length === 1 ? t('dose') : t('doses')} - - )} - {!day.isTemplate && Math.abs(totalMgDiff) > 0.1 ? ( - - - - - - -

- {totalMgDiff > 0 ? '+' : ''}{totalMgDiff.toFixed(1)} mg {t('comparedToRegularPlan')} -

-
-
-
- ) : ( - - {day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg - - )} -
-
+ toggleDayCollapse(day.id)} + toggleLabel={collapsedDays.has(day.id) ? t('expandDay') : t('collapseDay')} + rightSection={ + <> {canAddDay && ( )} -
-
-
+ + } + > + + {t('day')} {dayIndex + 1} + + {!day.isTemplate && doseCountDiff !== 0 ? ( + + + + + + +

+ {doseCountDiff > 0 ? '+' : ''}{doseCountDiff} {Math.abs(doseCountDiff) === 1 ? t('dose') : t('doses')} {t('comparedToRegularPlan')} +

+
+
+
+ ) : ( + + {day.doses.length} {day.doses.length === 1 ? t('dose') : t('doses')} + + )} + {!day.isTemplate && Math.abs(totalMgDiff) > 0.1 ? ( + + + + + + +

+ {totalMgDiff > 0 ? '+' : ''}{totalMgDiff.toFixed(1)} mg {t('comparedToRegularPlan')} +

+
+
+
+ ) : ( + + {day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg + + )} + {!collapsedDays.has(day.id) && ( {/* Dose table header */} diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 4f5330e..29e76a7 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -10,15 +10,16 @@ */ import React from 'react'; -import { FormNumericInput } from './ui/form-numeric-input'; -import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; -import { Button } from './ui/button'; -import { Switch } from './ui/switch'; +import { Card, CardContent } from './ui/card'; import { Label } from './ui/label'; +import { Switch } from './ui/switch'; import { Separator } from './ui/separator'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; +import { Button } from './ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; -import { ChevronDown, ChevronUp, Info } from 'lucide-react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; +import { FormNumericInput } from './ui/form-numeric-input'; +import CollapsibleCardHeader from './ui/collapsible-card-header'; +import { Info } from 'lucide-react'; import { getDefaultState } from '../constants/defaults'; /** @@ -124,6 +125,46 @@ const Settings = ({ const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true); const [isAdvancedExpanded, setIsAdvancedExpanded] = React.useState(false); + // Load and persist settings card expansion states + React.useEffect(() => { + const savedStates = localStorage.getItem('settingsCardStates_v1'); + if (savedStates) { + try { + const states = JSON.parse(savedStates); + if (states.diagram !== undefined) setIsDiagramExpanded(states.diagram); + if (states.simulation !== undefined) setIsSimulationExpanded(states.simulation); + if (states.pharmacokinetic !== undefined) setIsPharmacokineticExpanded(states.pharmacokinetic); + if (states.advanced !== undefined) setIsAdvancedExpanded(states.advanced); + } catch (e) { + console.warn('Failed to load settings card states:', e); + } + } + }, []); + + const updateDiagramExpanded = (value: boolean) => { + setIsDiagramExpanded(value); + saveCardStates({ diagram: value, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded }); + }; + + const updateSimulationExpanded = (value: boolean) => { + setIsSimulationExpanded(value); + saveCardStates({ diagram: isDiagramExpanded, simulation: value, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded }); + }; + + const updatePharmacokineticExpanded = (value: boolean) => { + setIsPharmacokineticExpanded(value); + saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: value, advanced: isAdvancedExpanded }); + }; + + const updateAdvancedExpanded = (value: boolean) => { + setIsAdvancedExpanded(value); + saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: value }); + }; + + const saveCardStates = (states: any) => { + localStorage.setItem('settingsCardStates_v1', JSON.stringify(states)); + }; + // Create defaults object for translation interpolation const defaultsForT = getDefaultsForTranslation(pkParams, therapeuticRange, uiSettings); @@ -158,12 +199,11 @@ const Settings = ({
{/* Diagram Settings Card */} - setIsDiagramExpanded(!isDiagramExpanded)}> -
- {t('diagramSettings')} - {isDiagramExpanded ? : } -
-
+ updateDiagramExpanded(!isDiagramExpanded)} + /> {isDiagramExpanded && (
@@ -431,12 +471,11 @@ const Settings = ({ {/* Simulation Settings Card */} - setIsSimulationExpanded(!isSimulationExpanded)}> -
- {t('simulationSettings')} - {isSimulationExpanded ? : } -
-
+ updateSimulationExpanded(!isSimulationExpanded)} + /> {isSimulationExpanded && (
@@ -507,12 +546,11 @@ const Settings = ({ {/* Pharmacokinetic Settings Card */} - setIsPharmacokineticExpanded(!isPharmacokineticExpanded)}> -
- {t('pharmacokineticsSettings')} - {isPharmacokineticExpanded ? : } -
-
+ updatePharmacokineticExpanded(!isPharmacokineticExpanded)} + /> {isPharmacokineticExpanded && (

{t('dAmphetamineParameters')}

@@ -627,12 +665,11 @@ const Settings = ({ {/* Advanced Settings Card */} - setIsAdvancedExpanded(!isAdvancedExpanded)}> -
- {t('advancedSettings')} - {isAdvancedExpanded ? : } -
-
+ updateAdvancedExpanded(!isAdvancedExpanded)} + /> {isAdvancedExpanded && (
diff --git a/src/components/ui/collapsible-card-header.tsx b/src/components/ui/collapsible-card-header.tsx new file mode 100644 index 0000000..31c1590 --- /dev/null +++ b/src/components/ui/collapsible-card-header.tsx @@ -0,0 +1,59 @@ +/** + * CollapsibleCardHeader + * + * Shared header row with a title + chevron toggle, optional children after title/chevron, + * and an optional right section for action buttons. + */ +import React from 'react'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { CardHeader, CardTitle } from './card'; +import { cn } from '../../lib/utils'; + +interface CollapsibleCardHeaderProps { + title: React.ReactNode; + isCollapsed: boolean; + onToggle: () => void; + children?: React.ReactNode; + rightSection?: React.ReactNode; + className?: string; + titleClassName?: string; + toggleLabel?: string; +} + +const CollapsibleCardHeader: React.FC = ({ + title, + isCollapsed, + onToggle, + children, + rightSection, + className, + titleClassName, + toggleLabel +}) => { + const accessibilityProps = toggleLabel ? { title: toggleLabel, 'aria-label': toggleLabel } : {}; + + return ( + +
+
+ + {children} +
+ {rightSection &&
{rightSection}
} +
+
+ ); +}; + +export default CollapsibleCardHeader;