turash/bugulma/frontend/components/ui/Typography.tsx
2025-12-15 10:06:41 +01:00

178 lines
5.5 KiB
TypeScript

import { useTranslation } from '@/hooks/useI18n.tsx';
import { clsx } from 'clsx';
import React from 'react';
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type TranslationReplacements = Record<string, string | number>;
export interface HeadingProps extends Omit<React.HTMLAttributes<HTMLHeadingElement>, 'children'> {
level?: HeadingLevel;
as?: HeadingLevel;
/** Translation key (e.g., 'dashboard.title') */
tKey?: string;
/** Translation replacements for interpolation (e.g., { name: 'John' }) */
replacements?: TranslationReplacements;
/** Direct text content (alternative to tKey) */
children?: React.ReactNode;
className?: string;
}
const headingStyles: Record<HeadingLevel, string> = {
h1: 'font-serif text-2xl sm:text-3xl font-bold tracking-tight text-foreground',
h2: 'text-2xl font-serif font-semibold text-foreground',
h3: 'text-lg font-semibold text-foreground',
h4: 'text-base font-semibold text-foreground',
h5: 'text-sm font-semibold text-foreground',
h6: 'text-xs font-semibold text-foreground',
};
/**
* Heading component for semantic headings with consistent styling
* Supports both translation keys and direct text content
*
* @example
* <Heading tKey="dashboard.title" />
* <Heading tKey="dashboard.greeting" replacements={{ name: 'John' }} />
* <Heading level="h2">Direct Text</Heading>
*/
export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({ level = 'h1', as, tKey, replacements, children, className, ...props }, ref) => {
const { t } = useTranslation();
const Component = as || level;
const baseStyles = headingStyles[level];
// Use translation if tKey is provided, otherwise use children
// If both are provided, tKey takes precedence
const content = tKey ? t(tKey, replacements) : children;
return (
<Component ref={ref} className={clsx(baseStyles, className)} {...props}>
{content}
</Component>
);
}
);
Heading.displayName = 'Heading';
export type TextVariant = 'body' | 'small' | 'muted' | 'bold' | 'italic';
export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
variant?: TextVariant;
as?: 'p' | 'span' | 'div';
/** Translation key (e.g., 'dashboard.description') */
tKey?: string;
/** Translation replacements for interpolation (e.g., { count: 5 }) */
replacements?: TranslationReplacements;
/** Direct text content (alternative to tKey) */
children?: React.ReactNode;
className?: string;
}
const textStyles: Record<TextVariant, string> = {
body: 'text-base text-foreground',
small: 'text-sm text-foreground',
muted: 'text-sm text-muted-foreground',
bold: 'text-base font-semibold text-foreground',
italic: 'text-base text-muted-foreground italic',
};
/**
* Text component for consistent text styling
* Supports both translation keys and direct text content
*
* @example
* <Text tKey="dashboard.description" />
* <Text tKey="dashboard.itemsCount" replacements={{ count: 5 }} />
* <Text variant="muted">Direct text content</Text>
*/
export const Text = React.forwardRef<HTMLElement, TextProps>(
({ variant = 'body', as = 'p', tKey, replacements, children, className, ...props }, ref) => {
const { t } = useTranslation();
const Component = as;
const baseStyles = textStyles[variant];
// Use translation if tKey is provided, otherwise use children
// If both are provided, tKey takes precedence
const content = tKey ? t(tKey, replacements) : children;
return (
<Component ref={ref as any} className={clsx(baseStyles, className)} {...props}>
{content}
</Component>
);
}
);
Text.displayName = 'Text';
export interface PriceProps {
value: number;
currency?: string;
variant?: 'large' | 'medium' | 'small';
showUnit?: boolean;
/** Translation key for unit label (e.g., 'common.currency.unit') */
unitTKey?: string;
/** Direct unit text (alternative to unitTKey) */
unit?: string;
/** Use locale-aware number formatting based on current language */
useLocale?: boolean;
className?: string;
}
/**
* Price component for displaying currency values with consistent formatting
* Supports localized currency formatting and unit labels via translation keys
*
* @example
* <Price value={99.99} currency="EUR" />
* <Price value={99.99} showUnit unitTKey="common.currency.perUnit" />
* <Price value={99.99} showUnit unit="per hour" />
*/
export const Price: React.FC<PriceProps> = ({
value,
currency = 'EUR',
variant = 'large',
showUnit = false,
unitTKey,
unit,
useLocale = true,
className,
}) => {
const { t, lang } = useTranslation();
// Get locale for number formatting (e.g., 'en-US', 'ru-RU', 'tt-TT')
const localeMap: Record<string, string> = {
en: 'en-US',
ru: 'ru-RU',
tt: 'tt-TT',
};
const locale = useLocale ? localeMap[lang] || 'en-US' : 'en-US';
const formattedValue = new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
const sizeClasses = {
large: 'text-2xl font-bold',
medium: 'text-lg font-semibold',
small: 'text-base font-medium',
};
// Get unit label from translation or use direct unit
const unitLabel = unitTKey ? t(unitTKey) : unit;
return (
<span className={clsx(sizeClasses[variant], 'text-primary', className)}>
{formattedValue}
{showUnit && unitLabel && (
<span className="text-sm text-muted-foreground ml-1">{unitLabel}</span>
)}
</span>
);
};