Fix few issues
This commit is contained in:
190
frontend/src/utils/README.md
Normal file
190
frontend/src/utils/README.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Text Overflow Prevention System
|
||||
|
||||
This comprehensive system prevents text overflow in UI components across all screen sizes and content types.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { TextOverflowPrevention, ResponsiveText } from '../utils/textUtils';
|
||||
|
||||
// Automatic truncation for StatusCard
|
||||
const title = TextOverflowPrevention.statusCard.title("Very Long Product Name That Could Overflow");
|
||||
|
||||
// Responsive text component
|
||||
<ResponsiveText
|
||||
text="Long text that adapts to screen size"
|
||||
textType="title"
|
||||
truncationType="mobile"
|
||||
/>
|
||||
```
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Automatic Truncation Engines
|
||||
- **StatusCard**: Optimized for status cards (35 chars title, 50 chars subtitle)
|
||||
- **Mobile**: Aggressive truncation for mobile devices (25 chars title, 35 chars subtitle)
|
||||
- **Production**: Specialized for production content (equipment lists, staff lists)
|
||||
|
||||
### 2. Responsive Text Component
|
||||
- Automatically adjusts based on screen size
|
||||
- Supports tooltips for truncated content
|
||||
- Multiple text types (title, subtitle, label, metadata, action)
|
||||
|
||||
### 3. Screen-Size Detection
|
||||
- Mobile: < 768px
|
||||
- Tablet: 768px - 1024px
|
||||
- Desktop: > 1024px
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Truncation
|
||||
```typescript
|
||||
import { TextOverflowPrevention } from '../utils/textUtils';
|
||||
|
||||
// StatusCard truncation
|
||||
const title = TextOverflowPrevention.statusCard.title(longTitle);
|
||||
const subtitle = TextOverflowPrevention.statusCard.subtitle(longSubtitle);
|
||||
|
||||
// Mobile-optimized truncation
|
||||
const mobileTitle = TextOverflowPrevention.mobile.title(longTitle);
|
||||
|
||||
// Production-specific truncation
|
||||
const equipment = TextOverflowPrevention.production.equipmentList(['oven-01', 'mixer-02']);
|
||||
const staff = TextOverflowPrevention.production.staffList(['Juan', 'María', 'Carlos']);
|
||||
```
|
||||
|
||||
### ResponsiveText Component
|
||||
```tsx
|
||||
import { ResponsiveText } from '../components/ui';
|
||||
|
||||
// Basic usage
|
||||
<ResponsiveText
|
||||
text="Product Name That Could Be Very Long"
|
||||
textType="title"
|
||||
className="font-bold text-lg"
|
||||
/>
|
||||
|
||||
// Custom responsive lengths
|
||||
<ResponsiveText
|
||||
text="Long description text"
|
||||
maxLength={{ mobile: 30, tablet: 50, desktop: 100 }}
|
||||
showTooltip={true}
|
||||
/>
|
||||
|
||||
// Different truncation types
|
||||
<ResponsiveText
|
||||
text="Equipment: oven-01, mixer-02, proofer-03"
|
||||
truncationType="production"
|
||||
textType="metadata"
|
||||
/>
|
||||
```
|
||||
|
||||
### Array Truncation
|
||||
```typescript
|
||||
import { truncateArray } from '../utils/textUtils';
|
||||
|
||||
// Truncate equipment list
|
||||
const equipment = truncateArray(['oven-01', 'mixer-02', 'proofer-03'], 2, 15);
|
||||
// Result: ['oven-01', 'mixer-02', '+1 más']
|
||||
|
||||
// Truncate staff list
|
||||
const staff = truncateArray(['Juan Perez', 'María González'], 1, 20);
|
||||
// Result: ['Juan Perez', '+1 más']
|
||||
```
|
||||
|
||||
### CSS Classes for Overflow Prevention
|
||||
```typescript
|
||||
import { overflowClasses } from '../utils/textUtils';
|
||||
|
||||
// Available classes
|
||||
overflowClasses.truncate // 'truncate'
|
||||
overflowClasses.breakWords // 'break-words'
|
||||
overflowClasses.ellipsis // 'overflow-hidden text-ellipsis whitespace-nowrap'
|
||||
overflowClasses.multilineEllipsis // 'overflow-hidden line-clamp-2'
|
||||
```
|
||||
|
||||
## Implementation in Components
|
||||
|
||||
### Enhanced StatusCard
|
||||
The StatusCard component now automatically:
|
||||
- Truncates titles, subtitles, and metadata
|
||||
- Adjusts truncation based on screen size
|
||||
- Shows tooltips for truncated content
|
||||
- Limits metadata items (3 on mobile, 4 on desktop)
|
||||
- Provides responsive action button labels
|
||||
|
||||
### Production Components
|
||||
Production components use specialized truncation:
|
||||
- Equipment lists: Max 3 items, 20 chars each
|
||||
- Staff lists: Max 3 items, 25 chars each
|
||||
- Batch numbers: Max 20 chars
|
||||
- Product names: Max 30 chars with word preservation
|
||||
|
||||
## Configuration
|
||||
|
||||
### Truncation Lengths
|
||||
|
||||
| Context | Mobile | Desktop | Preserve Words |
|
||||
|---------|--------|---------|----------------|
|
||||
| Title | 25 chars | 35 chars | Yes |
|
||||
| Subtitle | 35 chars | 50 chars | Yes |
|
||||
| Primary Label | 8 chars | 12 chars | No |
|
||||
| Secondary Label | 10 chars | 15 chars | No |
|
||||
| Metadata | 45 chars | 60 chars | Yes |
|
||||
| Actions | 8 chars | 12 chars | No |
|
||||
|
||||
### Custom Configuration
|
||||
```typescript
|
||||
import { truncateText, TruncateOptions } from '../utils/textUtils';
|
||||
|
||||
const options: TruncateOptions = {
|
||||
maxLength: 25,
|
||||
suffix: '...',
|
||||
preserveWords: true
|
||||
};
|
||||
|
||||
const result = truncateText("Very long text content", options);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use the system**: Don't implement manual truncation
|
||||
2. **Choose the right engine**: StatusCard for cards, Mobile for aggressive truncation, Production for specialized content
|
||||
3. **Enable tooltips**: Users should be able to see full content on hover
|
||||
4. **Test on mobile**: Always verify truncation works on small screens
|
||||
5. **Preserve word boundaries**: Use `preserveWords: true` for readable text
|
||||
|
||||
## Maintenance
|
||||
|
||||
To add new truncation types:
|
||||
|
||||
1. Add method to `TextOverflowPrevention` class
|
||||
2. Update `ResponsiveText` component to support new type
|
||||
3. Add configuration to truncation engines
|
||||
4. Update this documentation
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Manual Truncation
|
||||
```typescript
|
||||
// Before
|
||||
const title = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
||||
|
||||
// After
|
||||
const title = TextOverflowPrevention.statusCard.title(text);
|
||||
```
|
||||
|
||||
### From Basic Truncate Classes
|
||||
```tsx
|
||||
// Before
|
||||
<div className="truncate" title={text}>{text}</div>
|
||||
|
||||
// After
|
||||
<ResponsiveText text={text} textType="title" />
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Modern browsers with CSS Grid and Flexbox support
|
||||
- Mobile Safari, Chrome Mobile, Firefox Mobile
|
||||
- Responsive design works from 320px to 1920px+ screen widths
|
||||
191
frontend/src/utils/textUtils.ts
Normal file
191
frontend/src/utils/textUtils.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Text Overflow Prevention Utilities
|
||||
* Comprehensive system to prevent text overflow in UI components
|
||||
*/
|
||||
|
||||
export interface TruncateOptions {
|
||||
maxLength: number;
|
||||
suffix?: string;
|
||||
preserveWords?: boolean;
|
||||
}
|
||||
|
||||
export interface ResponsiveTruncateOptions {
|
||||
mobile: number;
|
||||
tablet: number;
|
||||
desktop: number;
|
||||
suffix?: string;
|
||||
preserveWords?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text to a specific length with ellipsis
|
||||
*/
|
||||
export const truncateText = (
|
||||
text: string | null | undefined,
|
||||
options: TruncateOptions
|
||||
): string => {
|
||||
if (!text) return '';
|
||||
|
||||
const { maxLength, suffix = '...', preserveWords = false } = options;
|
||||
|
||||
if (text.length <= maxLength) return text;
|
||||
|
||||
let truncated = text.slice(0, maxLength - suffix.length);
|
||||
|
||||
if (preserveWords) {
|
||||
const lastSpaceIndex = truncated.lastIndexOf(' ');
|
||||
if (lastSpaceIndex > 0) {
|
||||
truncated = truncated.slice(0, lastSpaceIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return truncated + suffix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get responsive truncate length based on screen size
|
||||
*/
|
||||
export const getResponsiveTruncateLength = (
|
||||
options: ResponsiveTruncateOptions,
|
||||
screenSize: 'mobile' | 'tablet' | 'desktop' = 'mobile'
|
||||
): number => {
|
||||
return options[screenSize];
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncate text responsively based on screen size
|
||||
*/
|
||||
export const truncateResponsive = (
|
||||
text: string | null | undefined,
|
||||
options: ResponsiveTruncateOptions,
|
||||
screenSize: 'mobile' | 'tablet' | 'desktop' = 'mobile'
|
||||
): string => {
|
||||
const maxLength = getResponsiveTruncateLength(options, screenSize);
|
||||
return truncateText(text, {
|
||||
maxLength,
|
||||
suffix: options.suffix,
|
||||
preserveWords: options.preserveWords
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncate array of strings (for metadata, tags, etc.)
|
||||
*/
|
||||
export const truncateArray = (
|
||||
items: string[],
|
||||
maxItems: number,
|
||||
maxItemLength?: number
|
||||
): string[] => {
|
||||
let result = items.slice(0, maxItems);
|
||||
|
||||
if (maxItemLength) {
|
||||
result = result.map(item =>
|
||||
truncateText(item, { maxLength: maxItemLength, preserveWords: true })
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length > maxItems) {
|
||||
result.push(`+${items.length - maxItems} más`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Smart truncation for different content types
|
||||
*/
|
||||
export class TextOverflowPrevention {
|
||||
// StatusCard specific truncation
|
||||
static statusCard = {
|
||||
title: (text: string) => truncateText(text, { maxLength: 35, preserveWords: true }),
|
||||
subtitle: (text: string) => truncateText(text, { maxLength: 50, preserveWords: true }),
|
||||
primaryValueLabel: (text: string) => truncateText(text, { maxLength: 12 }),
|
||||
secondaryLabel: (text: string) => truncateText(text, { maxLength: 15 }),
|
||||
secondaryValue: (text: string) => truncateText(text, { maxLength: 25, preserveWords: true }),
|
||||
metadataItem: (text: string) => truncateText(text, { maxLength: 60, preserveWords: true }),
|
||||
actionLabel: (text: string) => truncateText(text, { maxLength: 12 }),
|
||||
progressLabel: (text: string) => truncateText(text, { maxLength: 30, preserveWords: true }),
|
||||
};
|
||||
|
||||
// Mobile specific truncation (more aggressive)
|
||||
static mobile = {
|
||||
title: (text: string) => truncateText(text, { maxLength: 25, preserveWords: true }),
|
||||
subtitle: (text: string) => truncateText(text, { maxLength: 35, preserveWords: true }),
|
||||
primaryValueLabel: (text: string) => truncateText(text, { maxLength: 8 }),
|
||||
secondaryLabel: (text: string) => truncateText(text, { maxLength: 10 }),
|
||||
secondaryValue: (text: string) => truncateText(text, { maxLength: 20, preserveWords: true }),
|
||||
metadataItem: (text: string) => truncateText(text, { maxLength: 45, preserveWords: true }),
|
||||
actionLabel: (text: string) => truncateText(text, { maxLength: 8 }),
|
||||
progressLabel: (text: string) => truncateText(text, { maxLength: 25, preserveWords: true }),
|
||||
equipment: (items: string[]) => truncateArray(items, 2, 15).join(', '),
|
||||
staff: (items: string[]) => truncateArray(items, 2, 20).join(', '),
|
||||
};
|
||||
|
||||
// Production specific truncation
|
||||
static production = {
|
||||
equipmentList: (items: string[]) => truncateArray(items, 3, 20).join(', '),
|
||||
staffList: (items: string[]) => truncateArray(items, 3, 25).join(', '),
|
||||
batchNumber: (text: string) => truncateText(text, { maxLength: 20 }),
|
||||
productName: (text: string) => truncateText(text, { maxLength: 30, preserveWords: true }),
|
||||
notes: (text: string) => truncateText(text, { maxLength: 100, preserveWords: true }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class utilities for overflow prevention
|
||||
*/
|
||||
export const overflowClasses = {
|
||||
truncate: 'truncate',
|
||||
truncateWithTooltip: 'truncate cursor-help',
|
||||
breakWords: 'break-words',
|
||||
breakAll: 'break-all',
|
||||
wrapAnywhere: 'break-words hyphens-auto',
|
||||
ellipsis: 'overflow-hidden text-ellipsis whitespace-nowrap',
|
||||
multilineEllipsis: 'overflow-hidden line-clamp-2',
|
||||
responsiveText: 'text-sm sm:text-base lg:text-lg',
|
||||
responsiveTruncate: 'truncate sm:text-clip lg:text-clip',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Generate responsive classes for different screen sizes
|
||||
*/
|
||||
export const getResponsiveClasses = (
|
||||
baseClasses: string,
|
||||
mobileClasses?: string,
|
||||
tabletClasses?: string,
|
||||
desktopClasses?: string
|
||||
): string => {
|
||||
return [
|
||||
baseClasses,
|
||||
mobileClasses,
|
||||
tabletClasses && `sm:${tabletClasses}`,
|
||||
desktopClasses && `lg:${desktopClasses}`,
|
||||
].filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook-like function to determine screen size for truncation
|
||||
*/
|
||||
export const getScreenSize = (): 'mobile' | 'tablet' | 'desktop' => {
|
||||
if (typeof window === 'undefined') return 'desktop';
|
||||
|
||||
const width = window.innerWidth;
|
||||
if (width < 768) return 'mobile';
|
||||
if (width < 1024) return 'tablet';
|
||||
return 'desktop';
|
||||
};
|
||||
|
||||
/**
|
||||
* Safe text rendering with overflow prevention
|
||||
*/
|
||||
export const safeText = (
|
||||
text: string | null | undefined,
|
||||
fallback: string = '',
|
||||
maxLength?: number
|
||||
): string => {
|
||||
if (!text) return fallback;
|
||||
if (!maxLength) return text;
|
||||
return truncateText(text, { maxLength, preserveWords: true });
|
||||
};
|
||||
|
||||
export default TextOverflowPrevention;
|
||||
Reference in New Issue
Block a user