🤖 Pull request created from `various-fixes` containing PageHead
breadcrumbs and navigation alignment fixes, Umami analytics proxy, and
infrastructure updates.
141 lines
3.4 KiB
TypeScript
141 lines
3.4 KiB
TypeScript
import { cn, genericMemo, castMutableRef } from '@ermnvldmr/stl';
|
|
import React, { forwardRef, useEffect, useRef } from 'react';
|
|
|
|
import {
|
|
getTypographyClassNames,
|
|
type TextType,
|
|
type TextSize,
|
|
type TextColor,
|
|
type TextAlign,
|
|
type TextWrap,
|
|
type TextOverflow,
|
|
} from '../../lib/typography';
|
|
|
|
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
|
|
|
export type { TextType, TextSize, TextColor, TextAlign, TextWrap, TextOverflow };
|
|
|
|
/**
|
|
* Props for the Text component.
|
|
*/
|
|
export interface TextProps
|
|
extends ClassNameProps,
|
|
TestIdProps,
|
|
Omit<React.AllHTMLAttributes<HTMLElement>, 'as' | 'type' | 'size' | 'color'> {
|
|
/** Content to be rendered */
|
|
children: React.ReactNode;
|
|
/** Typography style type */
|
|
type?: TextType;
|
|
/** Typography size variant */
|
|
size?: TextSize;
|
|
/** Text color variant */
|
|
color?: TextColor;
|
|
/** Whether the text should be bold */
|
|
bold?: boolean;
|
|
/** Whether the text should be italic */
|
|
italic?: boolean;
|
|
/** Whether the text should be underlined */
|
|
underline?: boolean;
|
|
/** Whether the text should have a strike-through */
|
|
strike?: boolean;
|
|
/** The HTML element to use for rendering */
|
|
as?: React.ElementType;
|
|
/** Optional href for links */
|
|
href?: string;
|
|
/** Text alignment */
|
|
align?: TextAlign;
|
|
/** Text wrapping behavior */
|
|
wrap?: TextWrap;
|
|
/** Overflow behavior for single lines */
|
|
overflow?: TextOverflow;
|
|
/** Maximum number of lines to show (truncates with ellipsis) */
|
|
maxLines?: number;
|
|
/** Delay in milliseconds before the text appears after entering the viewport. */
|
|
delay?: number;
|
|
/** Underlying element's HTML type attribute (e.g. "button", "submit") */
|
|
htmlType?: string;
|
|
}
|
|
|
|
/**
|
|
* A flexible typography component that implements the design system's type scale.
|
|
*
|
|
* Text centralizes all typography rules — font families, sizes, line heights,
|
|
* letter spacing, colors, and decorations — via the getTypographyClassNames utility.
|
|
*/
|
|
const TextComponent = forwardRef<HTMLElement, TextProps>(function Text(
|
|
{
|
|
children,
|
|
type,
|
|
size,
|
|
color,
|
|
bold = false,
|
|
italic = false,
|
|
underline = false,
|
|
strike = false,
|
|
align,
|
|
wrap,
|
|
overflow,
|
|
maxLines,
|
|
delay,
|
|
as: Component = 'span',
|
|
htmlType,
|
|
className,
|
|
'data-testid': testId,
|
|
...props
|
|
},
|
|
ref
|
|
) {
|
|
const internalRef = useRef<HTMLElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!ref) return;
|
|
if (typeof ref === 'function') {
|
|
ref(internalRef.current);
|
|
} else {
|
|
castMutableRef<HTMLElement>(ref).current = internalRef.current;
|
|
}
|
|
}, [ref]);
|
|
|
|
const delayStyle: React.CSSProperties | undefined =
|
|
delay !== undefined
|
|
? {
|
|
animationDelay: `${delay}ms`,
|
|
animationFillMode: 'both',
|
|
}
|
|
: undefined;
|
|
|
|
return (
|
|
<Component
|
|
{...props}
|
|
ref={internalRef}
|
|
className={cn(
|
|
getTypographyClassNames({
|
|
type,
|
|
size,
|
|
color,
|
|
bold,
|
|
italic,
|
|
underline,
|
|
strike,
|
|
align,
|
|
wrap,
|
|
overflow,
|
|
maxLines,
|
|
}),
|
|
delay !== undefined && 'animate-in fade-in slide-in-from-bottom-2 duration-1000 ease-out',
|
|
className
|
|
)}
|
|
data-testid={testId}
|
|
style={{
|
|
...delayStyle,
|
|
...props.style,
|
|
}}
|
|
type={htmlType ?? (Component === 'button' ? 'button' : undefined)}
|
|
>
|
|
{children}
|
|
</Component>
|
|
);
|
|
});
|
|
|
|
export const Text = genericMemo(TextComponent);
|