Add collapsible-card-header component to consolidate and improve folding
This commit is contained in:
@@ -10,12 +10,13 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from './ui/button';
|
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 { Badge } from './ui/badge';
|
||||||
import { FormTimeInput } from './ui/form-time-input';
|
import { FormTimeInput } from './ui/form-time-input';
|
||||||
import { FormNumericInput } from './ui/form-numeric-input';
|
import { FormNumericInput } from './ui/form-numeric-input';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
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';
|
import type { DayGroup } from '../constants/defaults';
|
||||||
|
|
||||||
interface DayScheduleProps {
|
interface DayScheduleProps {
|
||||||
@@ -46,6 +47,23 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
// Track collapsed state for each day (by day ID)
|
// Track collapsed state for each day (by day ID)
|
||||||
const [collapsedDays, setCollapsedDays] = React.useState<Set<string>>(new Set());
|
const [collapsedDays, setCollapsedDays] = React.useState<Set<string>>(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<string>) => {
|
||||||
|
localStorage.setItem('dayScheduleCollapsedDays_v1', JSON.stringify(Array.from(newCollapsedDays)));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleDayCollapse = (dayId: string) => {
|
const toggleDayCollapse = (dayId: string) => {
|
||||||
setCollapsedDays(prev => {
|
setCollapsedDays(prev => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
@@ -54,6 +72,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
newSet.add(dayId);
|
newSet.add(dayId);
|
||||||
}
|
}
|
||||||
|
saveCollapsedDays(newSet);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -89,26 +108,37 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={day.id}>
|
<Card key={day.id}>
|
||||||
<CardHeader className="pb-3">
|
<CollapsibleCardHeader
|
||||||
<div className="flex items-center justify-between">
|
title={day.isTemplate ? t('regularPlan') : t('alternativePlan')}
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
isCollapsed={collapsedDays.has(day.id)}
|
||||||
|
onToggle={() => toggleDayCollapse(day.id)}
|
||||||
|
toggleLabel={collapsedDays.has(day.id) ? t('expandDay') : t('collapseDay')}
|
||||||
|
rightSection={
|
||||||
|
<>
|
||||||
|
{canAddDay && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
onClick={() => onAddDay(day.id)}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
className="h-6 w-6 p-0"
|
title={t('cloneDay')}
|
||||||
onClick={() => toggleDayCollapse(day.id)}
|
|
||||||
title={collapsedDays.has(day.id) ? t('expandDay') : t('collapseDay')}
|
|
||||||
>
|
>
|
||||||
{collapsedDays.has(day.id) ? (
|
<Copy className="h-4 w-4" />
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
<CardTitle className="text-lg">
|
)}
|
||||||
{day.isTemplate ? t('regularPlan') : t('alternativePlan')}
|
{!day.isTemplate && (
|
||||||
</CardTitle>
|
<Button
|
||||||
|
onClick={() => onRemoveDay(day.id)}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
|
title={t('removeDay')}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
{t('day')} {dayIndex + 1}
|
{t('day')} {dayIndex + 1}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -170,32 +200,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
{day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg
|
{day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</CollapsibleCardHeader>
|
||||||
<div className="flex gap-2">
|
|
||||||
{canAddDay && (
|
|
||||||
<Button
|
|
||||||
onClick={() => onAddDay(day.id)}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
title={t('cloneDay')}
|
|
||||||
>
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!day.isTemplate && (
|
|
||||||
<Button
|
|
||||||
onClick={() => onRemoveDay(day.id)}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
|
||||||
title={t('removeDay')}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
{!collapsedDays.has(day.id) && (
|
{!collapsedDays.has(day.id) && (
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{/* Dose table header */}
|
{/* Dose table header */}
|
||||||
|
|||||||
@@ -10,15 +10,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormNumericInput } from './ui/form-numeric-input';
|
import { Card, CardContent } from './ui/card';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { Switch } from './ui/switch';
|
|
||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
|
import { Switch } from './ui/switch';
|
||||||
import { Separator } from './ui/separator';
|
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 { 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';
|
import { getDefaultState } from '../constants/defaults';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,6 +125,46 @@ const Settings = ({
|
|||||||
const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true);
|
const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true);
|
||||||
const [isAdvancedExpanded, setIsAdvancedExpanded] = React.useState(false);
|
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
|
// Create defaults object for translation interpolation
|
||||||
const defaultsForT = getDefaultsForTranslation(pkParams, therapeuticRange, uiSettings);
|
const defaultsForT = getDefaultsForTranslation(pkParams, therapeuticRange, uiSettings);
|
||||||
|
|
||||||
@@ -158,12 +199,11 @@ const Settings = ({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Diagram Settings Card */}
|
{/* Diagram Settings Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="cursor-pointer pb-3" onClick={() => setIsDiagramExpanded(!isDiagramExpanded)}>
|
<CollapsibleCardHeader
|
||||||
<div className="flex items-center justify-between">
|
title={t('diagramSettings')}
|
||||||
<CardTitle className="text-lg">{t('diagramSettings')}</CardTitle>
|
isCollapsed={!isDiagramExpanded}
|
||||||
{isDiagramExpanded ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
onToggle={() => updateDiagramExpanded(!isDiagramExpanded)}
|
||||||
</div>
|
/>
|
||||||
</CardHeader>
|
|
||||||
{isDiagramExpanded && (
|
{isDiagramExpanded && (
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -431,12 +471,11 @@ const Settings = ({
|
|||||||
|
|
||||||
{/* Simulation Settings Card */}
|
{/* Simulation Settings Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="cursor-pointer pb-3" onClick={() => setIsSimulationExpanded(!isSimulationExpanded)}>
|
<CollapsibleCardHeader
|
||||||
<div className="flex items-center justify-between">
|
title={t('simulationSettings')}
|
||||||
<CardTitle className="text-lg">{t('simulationSettings')}</CardTitle>
|
isCollapsed={!isSimulationExpanded}
|
||||||
{isSimulationExpanded ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
onToggle={() => updateSimulationExpanded(!isSimulationExpanded)}
|
||||||
</div>
|
/>
|
||||||
</CardHeader>
|
|
||||||
{isSimulationExpanded && (
|
{isSimulationExpanded && (
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -507,12 +546,11 @@ const Settings = ({
|
|||||||
|
|
||||||
{/* Pharmacokinetic Settings Card */}
|
{/* Pharmacokinetic Settings Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="cursor-pointer pb-3" onClick={() => setIsPharmacokineticExpanded(!isPharmacokineticExpanded)}>
|
<CollapsibleCardHeader
|
||||||
<div className="flex items-center justify-between">
|
title={t('pharmacokineticsSettings')}
|
||||||
<CardTitle className="text-lg">{t('pharmacokineticsSettings')}</CardTitle>
|
isCollapsed={!isPharmacokineticExpanded}
|
||||||
{isPharmacokineticExpanded ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
onToggle={() => updatePharmacokineticExpanded(!isPharmacokineticExpanded)}
|
||||||
</div>
|
/>
|
||||||
</CardHeader>
|
|
||||||
{isPharmacokineticExpanded && (
|
{isPharmacokineticExpanded && (
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<h3 className="text-lg font-semibold">{t('dAmphetamineParameters')}</h3>
|
<h3 className="text-lg font-semibold">{t('dAmphetamineParameters')}</h3>
|
||||||
@@ -627,12 +665,11 @@ const Settings = ({
|
|||||||
|
|
||||||
{/* Advanced Settings Card */}
|
{/* Advanced Settings Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="cursor-pointer pb-3" onClick={() => setIsAdvancedExpanded(!isAdvancedExpanded)}>
|
<CollapsibleCardHeader
|
||||||
<div className="flex items-center justify-between">
|
title={t('advancedSettings')}
|
||||||
<CardTitle className="text-lg">{t('advancedSettings')}</CardTitle>
|
isCollapsed={!isAdvancedExpanded}
|
||||||
{isAdvancedExpanded ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
onToggle={() => updateAdvancedExpanded(!isAdvancedExpanded)}
|
||||||
</div>
|
/>
|
||||||
</CardHeader>
|
|
||||||
{isAdvancedExpanded && (
|
{isAdvancedExpanded && (
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 text-sm">
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 text-sm">
|
||||||
|
|||||||
59
src/components/ui/collapsible-card-header.tsx
Normal file
59
src/components/ui/collapsible-card-header.tsx
Normal file
@@ -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<CollapsibleCardHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
isCollapsed,
|
||||||
|
onToggle,
|
||||||
|
children,
|
||||||
|
rightSection,
|
||||||
|
className,
|
||||||
|
titleClassName,
|
||||||
|
toggleLabel
|
||||||
|
}) => {
|
||||||
|
const accessibilityProps = toggleLabel ? { title: toggleLabel, 'aria-label': toggleLabel } : {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardHeader className={cn('pb-3', className)}>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap flex-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToggle}
|
||||||
|
className="inline-flex items-center gap-2 rounded-md px-1 py-1 -ml-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:bg-muted/60 cursor-pointer"
|
||||||
|
aria-expanded={!isCollapsed}
|
||||||
|
{...accessibilityProps}
|
||||||
|
>
|
||||||
|
<CardTitle className={cn('text-lg', titleClassName)}>
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
{isCollapsed ? <ChevronDown className="h-5 w-5 flex-shrink-0" /> : <ChevronUp className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{rightSection && <div className="flex items-center gap-2">{rightSection}</div>}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollapsibleCardHeader;
|
||||||
Reference in New Issue
Block a user