Update consolidated and improved tooltips

This commit is contained in:
2026-01-17 13:06:56 +00:00
parent 3214c152dd
commit 3e7281e4db
6 changed files with 180 additions and 177 deletions

View File

@@ -19,6 +19,8 @@ import Settings from './components/settings';
import LanguageSelector from './components/language-selector';
import DisclaimerModal from './components/disclaimer-modal';
import { Button } from './components/ui/button';
import { TooltipProvider } from './components/ui/tooltip';
import { IconButtonWithTooltip } from './components/ui/icon-button-with-tooltip';
import { PROJECT_REPOSITORY_URL, APP_VERSION } from './constants/defaults';
// Custom Hooks
@@ -100,6 +102,7 @@ const MedPlanAssistant = () => {
} = useSimulation(appState);
return (
<TooltipProvider>
<div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8">
{/* Disclaimer Modal */}
<DisclaimerModal
@@ -147,15 +150,14 @@ const MedPlanAssistant = () => {
{t('both')}
</Button>
</div>
<Button
<IconButtonWithTooltip
onClick={() => updateUiSetting('stickyChart', !uiSettings.stickyChart)}
icon={uiSettings.stickyChart ? <Pin size={16} /> : <PinOff size={16} />}
tooltip={uiSettings.stickyChart ? t('unpinChart') : t('pinChart')}
variant={uiSettings.stickyChart ? 'default' : 'outline'}
size="sm"
className="shrink-0"
title={uiSettings.stickyChart ? t('unpinChart') : t('pinChart')}
>
{uiSettings.stickyChart ? <Pin size={16} /> : <PinOff size={16} />}
</Button>
/>
</div>
<SimulationChart
@@ -239,6 +241,7 @@ const MedPlanAssistant = () => {
</footer>
</div>
</div>
</TooltipProvider>
);
};

View File

@@ -14,7 +14,8 @@ 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 { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { IconButtonWithTooltip } from './ui/icon-button-with-tooltip';
import CollapsibleCardHeader from './ui/collapsible-card-header';
import { Plus, Copy, Trash2, ArrowDownAZ, TrendingUp, TrendingDown } from 'lucide-react';
import type { DayGroup } from '../constants/defaults';
@@ -116,25 +117,23 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
rightSection={
<>
{canAddDay && (
<Button
<IconButtonWithTooltip
onClick={() => onAddDay(day.id)}
icon={<Copy className="h-4 w-4" />}
tooltip={t('cloneDay')}
size="sm"
variant="outline"
title={t('cloneDay')}
>
<Copy className="h-4 w-4" />
</Button>
/>
)}
{!day.isTemplate && (
<Button
<IconButtonWithTooltip
onClick={() => onRemoveDay(day.id)}
icon={<Trash2 className="h-4 w-4" />}
tooltip={t('removeDay')}
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>
/>
)}
</>
}
@@ -143,7 +142,6 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
{t('day')} {dayIndex + 1}
</Badge>
{!day.isTemplate && doseCountDiff !== 0 ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -165,14 +163,12 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Badge variant="outline" className="text-xs">
{day.doses.length} {day.doses.length === 1 ? t('dose') : t('doses')}
</Badge>
)}
{!day.isTemplate && Math.abs(totalMgDiff) > 0.1 ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -194,7 +190,6 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Badge variant="outline" className="text-xs">
{day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg
@@ -207,7 +202,6 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
<div className="grid grid-cols-[120px_1fr_auto] gap-3 text-sm font-medium text-muted-foreground">
<div className="flex items-center gap-2">
<span>{t('time')}</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -231,7 +225,6 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div>{t('ldx')} (mg)</div>
<div></div>
@@ -267,16 +260,15 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
errorMessage={t('errorNumberRequired')}
warningMessage={t('warningZeroDose')}
/>
<Button
<IconButtonWithTooltip
onClick={() => onRemoveDose(day.id, dose.id)}
icon={<Trash2 className="h-4 w-4" />}
tooltip={t('removeDose')}
size="sm"
variant="ghost"
disabled={day.isTemplate && day.doses.length === 1}
className="h-9 w-9 p-0"
title={t('removeDose')}
>
<Trash2 className="h-4 w-4" />
</Button>
/>
</div>
);
})}

View File

@@ -15,7 +15,7 @@ import { Label } from './ui/label';
import { Switch } from './ui/switch';
import { Separator } from './ui/separator';
import { Button } from './ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { FormNumericInput } from './ui/form-numeric-input';
import CollapsibleCardHeader from './ui/collapsible-card-header';
@@ -269,7 +269,6 @@ const Settings = ({
<Label htmlFor="showTemplateDay" className="font-medium">
{t('showTemplateDayInChart')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'showTemplateDay'} onOpenChange={(open) => setOpenTooltipId(open ? 'showTemplateDay' : null)}>
<TooltipTrigger asChild>
<button
@@ -286,7 +285,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTemplateDayTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
@@ -300,7 +298,6 @@ const Settings = ({
<Label htmlFor="showDayReferenceLines" className="font-medium">
{t('showDayReferenceLines')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'showDayReferenceLines'} onOpenChange={(open) => setOpenTooltipId(open ? 'showDayReferenceLines' : null)}>
<TooltipTrigger asChild>
<button
@@ -317,7 +314,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showDayReferenceLinesTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
@@ -331,7 +327,6 @@ const Settings = ({
<Label htmlFor="showTherapeuticRange" className="font-medium">
{t('showTherapeuticRangeLines')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'showTherapeuticRangeLines'} onOpenChange={(open) => setOpenTooltipId(open ? 'showTherapeuticRangeLines' : null)}>
<TooltipTrigger asChild>
<button
@@ -348,7 +343,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTherapeuticRangeLinesTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{showTherapeuticRange && (
<div className="ml-8 space-y-2">
@@ -356,7 +350,6 @@ const Settings = ({
<Label className="font-medium">
{t('therapeuticRange')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'therapeuticRange'} onOpenChange={(open) => setOpenTooltipId(open ? 'therapeuticRange' : null)}>
<TooltipTrigger asChild>
<button
@@ -373,7 +366,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'therapeuticRangeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap items-center gap-2">
<FormNumericInput
@@ -406,7 +398,6 @@ const Settings = ({
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="font-medium">{t('displayedDays')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'displayedDays'} onOpenChange={(open) => setOpenTooltipId(open ? 'displayedDays' : null)}>
<TooltipTrigger asChild>
<button
@@ -423,7 +414,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'displayedDaysTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={displayedDays}
@@ -440,7 +430,6 @@ const Settings = ({
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="font-medium">{t('yAxisRange')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'yAxisRange'} onOpenChange={(open) => setOpenTooltipId(open ? 'yAxisRange' : null)}>
<TooltipTrigger asChild>
<button
@@ -457,7 +446,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'yAxisRangeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap items-center gap-2">
<FormNumericInput
@@ -487,7 +475,6 @@ const Settings = ({
<div className="space-y-2">
<Label className="font-medium">{t('xAxisTimeFormat')}</Label>
<TooltipProvider>
<Select
value={showDayTimeOnXAxis}
onValueChange={value => onUpdateUiSetting('showDayTimeOnXAxis', value)}
@@ -528,7 +515,6 @@ const Settings = ({
</Tooltip>
</SelectContent>
</Select>
</TooltipProvider>
</div>
</CardContent>
)}
@@ -546,7 +532,6 @@ const Settings = ({
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="font-medium">{t('simulationDuration')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'simulationDuration'} onOpenChange={(open) => setOpenTooltipId(open ? 'simulationDuration' : null)}>
<TooltipTrigger asChild>
<button
@@ -563,7 +548,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'simulationDurationTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={simulationDays}
@@ -598,7 +582,6 @@ const Settings = ({
<Label htmlFor="steadyStateDaysEnabled" className="font-medium">
{t('steadyStateDays')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'steadyStateDays'} onOpenChange={(open) => setOpenTooltipId(open ? 'steadyStateDays' : null)}>
<TooltipTrigger asChild>
<button
@@ -615,7 +598,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'steadyStateDaysTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{steadyStateDaysEnabled && (
<div className="ml-8 space-y-2">
@@ -648,7 +630,6 @@ const Settings = ({
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="font-medium">{t('halfLife')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'halfLife'} onOpenChange={(open) => setOpenTooltipId(open ? 'halfLife' : null)}>
<TooltipTrigger asChild>
<button
@@ -665,7 +646,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'halfLifeTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={pkParams.damph.halfLife}
@@ -688,7 +668,6 @@ const Settings = ({
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="font-medium">{t('conversionHalfLife')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'conversionHalfLife'} onOpenChange={(open) => setOpenTooltipId(open ? 'conversionHalfLife' : null)}>
<TooltipTrigger asChild>
<button
@@ -705,7 +684,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'conversionHalfLifeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={pkParams.ldx.halfLife}
@@ -724,7 +702,6 @@ const Settings = ({
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="font-medium">{t('absorptionHalfLife')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'absorptionHalfLife'} onOpenChange={(open) => setOpenTooltipId(open ? 'absorptionHalfLife' : null)}>
<TooltipTrigger asChild>
<button
@@ -741,7 +718,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'absorptionHalfLifeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={pkParams.ldx.absorptionHalfLife}
@@ -784,7 +760,6 @@ const Settings = ({
<Label htmlFor="weightBasedVdEnabled" className="font-medium">
{t('weightBasedVdScaling')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'weightBasedVd'} onOpenChange={(open) => setOpenTooltipId(open ? 'weightBasedVd' : null)}>
<TooltipTrigger asChild>
<button
@@ -801,13 +776,11 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'weightBasedVdTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{pkParams.advanced.weightBasedVd.enabled && (
<div className="ml-8 space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm font-medium">{t('bodyWeight')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'bodyWeight'} onOpenChange={(open) => setOpenTooltipId(open ? 'bodyWeight' : null)}>
<TooltipTrigger asChild>
<button
@@ -824,7 +797,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'bodyWeightTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={pkParams.advanced.weightBasedVd.bodyWeight}
@@ -852,7 +824,6 @@ const Settings = ({
<Label htmlFor="foodEffectEnabled" className="font-medium">
{t('foodEffectEnabled')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'foodEffect'} onOpenChange={(open) => setOpenTooltipId(open ? 'foodEffect' : null)}>
<TooltipTrigger asChild>
<button
@@ -869,13 +840,11 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'foodEffectTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{pkParams.advanced.foodEffect.enabled && (
<div className="ml-8 space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm font-medium">{t('tmaxDelay')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'tmaxDelay'} onOpenChange={(open) => setOpenTooltipId(open ? 'tmaxDelay' : null)}>
<TooltipTrigger asChild>
<button
@@ -892,7 +861,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'tmaxDelayTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={pkParams.advanced.foodEffect.tmaxDelay}
@@ -920,7 +888,6 @@ const Settings = ({
<Label htmlFor="urinePHEnabled" className="font-medium">
{t('urinePHTendency')}
</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'urinePH'} onOpenChange={(open) => setOpenTooltipId(open ? 'urinePH' : null)}>
<TooltipTrigger asChild>
<button
@@ -937,13 +904,11 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'urinePHTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{pkParams.advanced.urinePh.enabled && (
<div className="ml-8 space-y-2">
<div className="flex items-center gap-2">
<Label className="text-sm font-medium">{t('urinePHValue')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'urinePHValue'} onOpenChange={(open) => setOpenTooltipId(open ? 'urinePHValue' : null)}>
<TooltipTrigger asChild>
<button
@@ -960,7 +925,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{tWithDefaults(t, 'urinePHValueTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={pkParams.advanced.urinePh.phTendency}
@@ -981,7 +945,6 @@ const Settings = ({
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label className="font-medium">{t('oralBioavailability')}</Label>
<TooltipProvider>
<Tooltip open={openTooltipId === 'oralBioavailability'} onOpenChange={(open) => setOpenTooltipId(open ? 'oralBioavailability' : null)}>
<TooltipTrigger asChild>
<button
@@ -998,7 +961,6 @@ const Settings = ({
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'oralBioavailabilityTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormNumericInput
value={pkParams.advanced.fOral}

View File

@@ -25,7 +25,6 @@ import {
Tooltip as UiTooltip,
TooltipTrigger as UiTooltipTrigger,
TooltipContent as UiTooltipContent,
TooltipProvider as UiTooltipProvider,
} from './ui/tooltip';
// Chart color scheme
@@ -310,12 +309,12 @@ const chartDomain = React.useMemo(() => {
? scrollableWidth
: Math.ceil((scrollableWidth / dispDays) * simDays);
// Render legend with tooltips for full names (custom legend renderer)
const renderLegend = React.useCallback((props: any) => {
const { payload } = props;
if (!payload) return null;
return (
<UiTooltipProvider>
<ul className="flex flex-wrap gap-2 text-xs leading-tight">
{payload.map((item: any) => {
const labelInfo = seriesLabels[item.dataKey] || { display: item.value, full: item.value };
@@ -331,7 +330,6 @@ const chartDomain = React.useMemo(() => {
<UiTooltipTrigger asChild>
<span
className="px-1 py-0.5 rounded-sm bg-white text-black shadow-sm border border-muted truncate inline-block max-w-[100px]"
title={labelInfo.full}
>
{labelInfo.display}
</span>
@@ -344,7 +342,6 @@ const chartDomain = React.useMemo(() => {
);
})}
</ul>
</UiTooltipProvider>
);
}, [seriesLabels]);

View File

@@ -11,6 +11,7 @@
import * as React from "react"
import { Minus, Plus, X } from "lucide-react"
import { Button } from "./button"
import { IconButtonWithTooltip } from "./icon-button-with-tooltip"
import { Input } from "./input"
import { cn } from "../../lib/utils"
import { useTranslation } from "react-i18next"
@@ -199,8 +200,10 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
<Plus className="h-4 w-4" />
</Button>
{clearButton && allowEmpty && (
<Button
<IconButtonWithTooltip
type="button"
icon={<X className="h-4 w-4" />}
tooltip={t('buttonClear')}
variant="outline"
size="icon"
className={cn(
@@ -210,10 +213,7 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
)}
onClick={() => onChange('')}
tabIndex={-1}
title={ t('buttonClear') }
>
<X className="h-4 w-4" />
</Button>
/>
)}
</div>
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}

View File

@@ -0,0 +1,49 @@
/**
* IconButtonWithTooltip
*
* A reusable button component that combines an icon button with a tooltip.
* Provides consistent tooltip behavior across the app for icon-only action buttons.
*
* @author Andreas Weyer
* @license MIT
*/
import React from 'react';
import { Button, ButtonProps } from './button';
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
interface IconButtonWithTooltipProps extends Omit<ButtonProps, 'children'> {
/** The icon element to display in the button */
icon: React.ReactNode;
/** The tooltip text to show on hover */
tooltip: string;
/** Optional side for tooltip positioning */
tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
}
export const IconButtonWithTooltip: React.FC<IconButtonWithTooltipProps> = ({
icon,
tooltip,
tooltipSide = 'top',
disabled,
...buttonProps
}) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={disabled}
aria-label={tooltip}
{...buttonProps}
>
{icon}
</Button>
</TooltipTrigger>
{!disabled && (
<TooltipContent side={tooltipSide}>
<p className="text-xs">{tooltip}</p>
</TooltipContent>
)}
</Tooltip>
);
};