Update various color/style improvements, primarily for error/warn/info bubbles and boxes
This commit is contained in:
161
docs/README.CSS_UTILITY_CLASSES.md
Normal file
161
docs/README.CSS_UTILITY_CLASSES.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Custom CSS Utility Classes
|
||||
|
||||
This document describes the centralized CSS utility classes defined in `src/styles/global.css` for consistent styling across the application.
|
||||
|
||||
## Error & Warning Classes
|
||||
|
||||
### Validation Bubbles (Popups)
|
||||
|
||||
**`.error-bubble`**
|
||||
- Used for error validation popup messages on form fields
|
||||
- Light mode: Soft red background with dark red text
|
||||
- Dark mode: Very dark red background (80% opacity) with light red text
|
||||
- Includes border for visual separation
|
||||
- Example: Input field validation errors
|
||||
|
||||
**`.warning-bubble`**
|
||||
- Used for warning validation popup messages on form fields
|
||||
- Light mode: Soft amber background with dark amber text
|
||||
- Dark mode: Very dark amber background (80% opacity) with light amber text
|
||||
- Includes border for visual separation
|
||||
- Example: Input field warnings about unusual values
|
||||
|
||||
### Borders
|
||||
|
||||
**`.error-border`**
|
||||
- Red border for form inputs with errors
|
||||
- Uses the `destructive` color from the theme
|
||||
- Example: Highlight invalid input fields
|
||||
|
||||
**`.warning-border`**
|
||||
- Amber border for form inputs with warnings
|
||||
- Uses `amber-500` color
|
||||
- Example: Highlight input fields with unusual but valid values
|
||||
|
||||
### Background Boxes (Static Sections)
|
||||
|
||||
**`.error-bg-box`**
|
||||
- For static error information sections
|
||||
- Light mode: Light red background
|
||||
- Dark mode: Dark red background (40% opacity)
|
||||
- Includes border
|
||||
- Example: Persistent error messages in modals
|
||||
|
||||
**`.warning-bg-box`**
|
||||
- For static warning information sections
|
||||
- Light mode: Light amber background
|
||||
- Dark mode: Dark amber background (40% opacity)
|
||||
- Includes border
|
||||
- Example: Warning boxes in settings
|
||||
|
||||
**`.info-bg-box`**
|
||||
- For informational sections
|
||||
- Light mode: Light blue background
|
||||
- Dark mode: Dark blue background (40% opacity)
|
||||
- Includes border
|
||||
- Example: Helpful tips, contextual information
|
||||
|
||||
### Text Colors
|
||||
|
||||
**`.error-text`**
|
||||
- Dark red text in light mode, light red in dark mode
|
||||
- High contrast for readability
|
||||
- Example: Inline error messages
|
||||
|
||||
**`.warning-text`**
|
||||
- Dark amber text in light mode, light amber in dark mode
|
||||
- High contrast for readability
|
||||
- Example: Inline warning messages
|
||||
|
||||
**`.info-text`**
|
||||
- Dark blue text in light mode, light blue in dark mode
|
||||
- High contrast for readability
|
||||
- Example: Inline informational text
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Form Validation Popup
|
||||
|
||||
```tsx
|
||||
{hasError && (
|
||||
<div className="error-bubble w-80 text-xs p-2 rounded-md">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasWarning && (
|
||||
<div className="warning-bubble w-80 text-xs p-2 rounded-md">
|
||||
{warningMessage}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### Input Field Borders
|
||||
|
||||
```tsx
|
||||
<Input
|
||||
className={cn(
|
||||
hasError && "error-border",
|
||||
hasWarning && !hasError && "warning-border"
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Static Information Boxes
|
||||
|
||||
```tsx
|
||||
{/* Warning box */}
|
||||
<div className="warning-bg-box rounded-md p-3">
|
||||
<p className="warning-text">{warningText}</p>
|
||||
</div>
|
||||
|
||||
{/* Info box */}
|
||||
<div className="info-bg-box rounded-md p-3">
|
||||
<p className="info-text">{infoText}</p>
|
||||
</div>
|
||||
|
||||
{/* Error box */}
|
||||
<div className="error-bg-box rounded-md p-3">
|
||||
<p className="error-text">{errorText}</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
All classes are designed with accessibility in mind:
|
||||
|
||||
- ✅ **High contrast ratios** - Meet WCAG AA standards for text readability
|
||||
- ✅ **Dark mode optimized** - Reduced saturation and brightness in dark mode (80% opacity for bubbles, 40% for boxes)
|
||||
- ✅ **Consistent theming** - Semantic color usage (red=error, amber=warning, blue=info)
|
||||
- ✅ **Icon visibility** - Muted backgrounds ensure icons stand out
|
||||
- ✅ **Border separation** - Clear visual boundaries between elements
|
||||
|
||||
## Opacity Rationale
|
||||
|
||||
- **Validation bubbles**: 80% opacity in dark mode - Higher opacity for better text readability during focused interaction
|
||||
- **Background boxes**: 40% opacity in dark mode - Lower opacity for persistent elements to reduce visual weight
|
||||
|
||||
## Migration Guide
|
||||
|
||||
When updating existing code to use these classes:
|
||||
|
||||
**Before:**
|
||||
```tsx
|
||||
className="bg-red-50 dark:bg-red-950/50 text-red-900 dark:text-red-200 border border-red-300 dark:border-red-800"
|
||||
```
|
||||
|
||||
**After:**
|
||||
```tsx
|
||||
className="error-bubble"
|
||||
```
|
||||
|
||||
This reduces duplication, ensures consistency, and makes it easier to update the design system in the future.
|
||||
|
||||
## Files Using These Classes
|
||||
|
||||
- `src/components/ui/form-numeric-input.tsx`
|
||||
- `src/components/ui/form-time-input.tsx`
|
||||
- `src/components/settings.tsx`
|
||||
- `src/components/data-management-modal.tsx`
|
||||
- `src/components/disclaimer-modal.tsx`
|
||||
- `src/components/day-schedule.tsx`
|
||||
@@ -320,7 +320,7 @@ const MedPlanAssistant = () => {
|
||||
|
||||
</div>
|
||||
|
||||
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm text-muted-foreground border">
|
||||
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm border">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2 text-foreground">{t('importantNote')}</h3>
|
||||
|
||||
@@ -843,7 +843,7 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
||||
className={`flex items-center gap-2 text-sm ${
|
||||
jsonValidationMessage.type === 'success'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
: 'error-text'
|
||||
}`}
|
||||
>
|
||||
{jsonValidationMessage.type === 'success' ? (
|
||||
@@ -858,7 +858,7 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
||||
{jsonValidationMessage.warnings.map((warning, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-yellow-500 text-white text-xs p-2 rounded-md"
|
||||
className="warning-bubble text-xs p-2 rounded-md"
|
||||
>
|
||||
{warning}
|
||||
</div>
|
||||
@@ -909,7 +909,7 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-5 w-5 text-destructive" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-destructive">{t('deleteSpecificData')}</h3>
|
||||
<h3 className="text-lg font-semibold">{t('deleteSpecificData')}</h3>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@@ -926,8 +926,8 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Warning Message */}
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 text-sm">
|
||||
<p className="text-yellow-800 dark:text-yellow-200">{t('deleteDataWarning')}</p>
|
||||
<div className="warning-bg-box rounded-md p-3 text-sm">
|
||||
<p className="warning-text">{t('deleteDataWarning')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -135,7 +135,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
tooltip={t('removeDay')}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -153,7 +153,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${doseCountDiff > 0 ? 'bg-blue-100 dark:bg-blue-900/40 dark:text-blue-200' : 'bg-orange-100 dark:bg-orange-900/40 dark:text-orange-200'}`}
|
||||
className={`text-xs ${doseCountDiff > 0 ? 'bg-blue-100 dark:bg-blue-900/60 dark:text-blue-200' : 'bg-orange-100 dark:bg-orange-900/60 dark:text-orange-200'}`}
|
||||
>
|
||||
{doseCountDiff > 0 ? <TrendingUp className="h-3 w-3 inline mr-1" /> : <TrendingDown className="h-3 w-3 inline mr-1" />}
|
||||
{day.doses.length} {day.doses.length === 1 ? t('dose') : t('doses')}
|
||||
@@ -180,7 +180,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${totalMgDiff > 0 ? 'bg-blue-100 dark:bg-blue-900/40 dark:text-blue-200' : 'bg-orange-100 dark:bg-orange-900/40 dark:text-orange-200'}`}
|
||||
className={`text-xs ${totalMgDiff > 0 ? 'bg-blue-100 dark:bg-blue-900/60 dark:text-blue-200' : 'bg-orange-100 dark:bg-orange-900/60 dark:text-orange-200'}`}
|
||||
>
|
||||
{totalMgDiff > 0 ? <TrendingUp className="h-3 w-3 inline mr-1" /> : <TrendingDown className="h-3 w-3 inline mr-1" />}
|
||||
{day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg
|
||||
@@ -291,7 +291,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={day.isTemplate && day.doses.length === 1}
|
||||
className="h-9 w-9 p-0 border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground disabled:border-muted"
|
||||
className="h-9 w-9 p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground disabled:border-muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ const DisclaimerModal: React.FC<DisclaimerModalProps> = ({
|
||||
<span className="text-2xl">⛔</span>
|
||||
{t('disclaimerModalScheduleII')}
|
||||
</h3>
|
||||
<p className="text-sm text-red-800 dark:text-red-300">
|
||||
<p className="text-sm error-text">
|
||||
{t('disclaimerModalScheduleIIText')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -772,8 +772,8 @@ const Settings = ({
|
||||
/>
|
||||
{isAdvancedExpanded && (
|
||||
<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">
|
||||
<p className="text-yellow-800 dark:text-yellow-200">{t('advancedSettingsWarning')}</p>
|
||||
<div className="warning-bg-box rounded-md p-3 text-sm">
|
||||
<p className="warning-text">{t('advancedSettingsWarning')}</p>
|
||||
</div>
|
||||
|
||||
{/* Standard Volume of Distribution */}
|
||||
@@ -816,7 +816,7 @@ const Settings = ({
|
||||
</SelectContent>
|
||||
</FormSelect>
|
||||
{pkParams.advanced.standardVd?.preset === 'weight-based' && (
|
||||
<div className="ml-0 mt-2 p-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded text-xs text-blue-800 dark:text-blue-200">
|
||||
<div className="ml-0 mt-2 p-2 info-bg-box rounded text-xs info-text">
|
||||
ⓘ {t('weightBasedVdInfo')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -206,8 +206,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-r-none border-r-0",
|
||||
hasError && "border-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500"
|
||||
hasError && "error-border",
|
||||
hasWarning && !hasError && "warning-border"
|
||||
)}
|
||||
onClick={() => updateValue(-1)}
|
||||
disabled={isAtMin}
|
||||
@@ -227,8 +227,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
inputWidth, "h-9 z-10",
|
||||
"rounded-none",
|
||||
getAlignmentClass(),
|
||||
hasError && "border-destructive focus-visible:ring-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500 focus-visible:ring-yellow-500"
|
||||
hasError && "error-border focus-visible:ring-destructive",
|
||||
hasWarning && !hasError && "warning-border focus-visible:ring-amber-500"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -239,8 +239,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
className={cn(
|
||||
"h-9 w-9",
|
||||
showResetButton ? "rounded-l-none rounded-r-none border-x-0" : "rounded-l-none border-l-0",
|
||||
hasError && "border-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500"
|
||||
hasError && "error-border",
|
||||
hasWarning && !hasError && "warning-border"
|
||||
)}
|
||||
onClick={() => updateValue(1)}
|
||||
disabled={isAtMax}
|
||||
@@ -257,8 +257,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-l-none",
|
||||
hasError && "border-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500"
|
||||
hasError && "error-border",
|
||||
hasWarning && !hasError && "warning-border"
|
||||
)}
|
||||
onClick={() => onChange(String(defaultValue ?? ''))}
|
||||
tabIndex={-1}
|
||||
@@ -267,12 +267,12 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
</div>
|
||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
||||
{hasError && isFocused && errorMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-20 w-64 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
||||
<div className="absolute top-full left-0 mt-1 z-20 w-80 error-bubble text-xs p-2 rounded-md shadow-lg">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{hasWarning && isFocused && warningMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-20 w-48 bg-yellow-500 text-white text-xs p-2 rounded-md shadow-lg">
|
||||
<div className="absolute top-full left-0 mt-1 z-20 w-80 warning-bubble text-xs p-2 rounded-md shadow-lg">
|
||||
{warningMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -38,9 +38,10 @@ export const FormSelect: React.FC<FormSelectProps> = ({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0">
|
||||
<Select value={value} onValueChange={onValueChange}>
|
||||
<Select value={value} onValueChange={onValueChange}>
|
||||
<SelectTrigger className={cn(
|
||||
showResetButton && "rounded-r-none border-r-0 z-10",
|
||||
"bg-background",
|
||||
triggerClassName
|
||||
)}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
|
||||
@@ -199,8 +199,8 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||
"w-20 h-9 z-20",
|
||||
"rounded-r-none",
|
||||
getAlignmentClass(),
|
||||
hasError && "border-destructive focus-visible:ring-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500 focus-visible:ring-yellow-500"
|
||||
hasError && "error-border focus-visible:ring-destructive",
|
||||
hasWarning && !hasError && "warning-border focus-visible:ring-amber-500"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -289,12 +289,12 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||
</div>
|
||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
||||
{hasError && isFocused && errorMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-80 error-bubble text-xs p-2 rounded-md shadow-lg">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{hasWarning && isFocused && warningMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-yellow-500 text-white text-xs p-2 rounded-md shadow-lg">
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-80 warning-bubble text-xs p-2 rounded-md shadow-lg">
|
||||
{warningMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
--accent-foreground: 0 0% 90%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--bubble-error: 0 84% 60%;
|
||||
--bubble-error-foreground: 0 0% 98%;
|
||||
--bubble-warning: 42 100% 60%;
|
||||
--bubble-warning-foreground: 0 0% 98%;
|
||||
--border: 0 0% 25%;
|
||||
--input: 0 0% 25%;
|
||||
--ring: 0 0% 40%;
|
||||
@@ -68,3 +72,55 @@
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Error message bubble - validation popups */
|
||||
.error-bubble {
|
||||
@apply bg-[hsl(var(--background))] text-[hsl(var(--foreground))] border border-red-500 dark:border-red-500;
|
||||
}
|
||||
|
||||
/* Warning message bubble - validation popups */
|
||||
.warning-bubble {
|
||||
@apply bg-[hsl(var(--background))] text-[hsl(var(--foreground))] border border-amber-500 dark:border-amber-500;
|
||||
}
|
||||
|
||||
/* Error border - for input fields with errors */
|
||||
.error-border {
|
||||
@apply !border-red-500;
|
||||
}
|
||||
|
||||
/* Warning border - for input fields with warnings */
|
||||
.warning-border {
|
||||
@apply !border-amber-500;
|
||||
}
|
||||
|
||||
/* Error background box - for static error/warning sections */
|
||||
.error-bg-box {
|
||||
@apply bg-[hsl(var(--background))] border border-red-500 dark:border-red-500;
|
||||
}
|
||||
|
||||
/* Warning background box - for static warning sections */
|
||||
.warning-bg-box {
|
||||
@apply bg-[hsl(var(--background))] border border-amber-500 dark:border-amber-500;
|
||||
}
|
||||
|
||||
/* Info background box - for informational sections */
|
||||
.info-bg-box {
|
||||
@apply bg-[hsl(var(--background))] border border-blue-500 dark:border-blue-500;
|
||||
}
|
||||
|
||||
/* Error text - for inline error text */
|
||||
.error-text {
|
||||
@apply text-[hsl(var(--foreground))];
|
||||
}
|
||||
|
||||
/* Warning text - for inline warning text */
|
||||
.warning-text {
|
||||
@apply text-[hsl(var(--foreground))];
|
||||
}
|
||||
|
||||
/* Info text - for inline info text */
|
||||
.info-text {
|
||||
@apply text-[hsl(var(--foreground))];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user