Files
gridpilot.gg/apps/website/ui/Heading.tsx
2026-01-21 01:27:08 +01:00

141 lines
4.6 KiB
TypeScript

import React, { ReactNode, forwardRef, CSSProperties } from 'react';
export interface HeadingProps {
children: ReactNode;
level?: 1 | 2 | 3 | 4 | 5 | 6;
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
align?: 'left' | 'center' | 'right';
uppercase?: boolean;
intent?: 'primary' | 'telemetry' | 'warning' | 'critical' | 'default';
truncate?: boolean;
icon?: ReactNode;
id?: string;
/** @deprecated Use semantic props instead. */
className?: string;
/** @deprecated Use semantic props instead. */
style?: CSSProperties;
/** @deprecated Use semantic props instead. */
mb?: number | string;
/** @deprecated Use semantic props instead. */
marginBottom?: number | string;
/** @deprecated Use semantic props instead. */
mt?: number | string;
/** @deprecated Use semantic props instead. */
marginTop?: number | string;
/** @deprecated Use semantic props instead. */
color?: string;
/** @deprecated Use semantic props instead. */
fontSize?: string | { base: string; sm?: string; md: string; lg?: string; xl?: string };
/** @deprecated Use semantic props instead. */
letterSpacing?: string;
/** @deprecated Use semantic props instead. */
size?: string;
/** @deprecated Use semantic props instead. */
groupHoverColor?: string;
/** @deprecated Use semantic props instead. */
lineHeight?: string | number;
/** @deprecated Use semantic props instead. */
transition?: boolean;
}
/**
* Heading - Redesigned for "Modern Precision" theme.
* Enforces semantic props.
*/
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
children,
level = 1,
weight = 'bold',
align = 'left',
uppercase = false,
intent = 'default',
truncate,
icon,
id,
className,
style: styleProp,
mb,
marginBottom,
mt,
marginTop,
color,
fontSize,
letterSpacing,
size,
groupHoverColor,
lineHeight,
transition,
}, ref) => {
const Tag = `h${level}` as const;
const weightClasses = {
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
bold: 'font-bold'
};
const sizeClasses = {
1: 'text-4xl md:text-5xl tracking-tighter leading-none',
2: 'text-3xl md:text-4xl tracking-tight leading-tight',
3: 'text-2xl md:text-3xl tracking-tight leading-snug',
4: 'text-xl md:text-2xl tracking-normal leading-normal',
5: 'text-lg md:text-xl tracking-normal leading-normal',
6: 'text-base md:text-lg tracking-wide leading-normal'
};
const intentClasses = {
default: 'text-[var(--ui-color-text-high)]',
primary: 'text-[var(--ui-color-intent-primary)]',
telemetry: 'text-[var(--ui-color-intent-telemetry)]',
warning: 'text-[var(--ui-color-intent-warning)]',
critical: 'text-[var(--ui-color-intent-critical)]',
};
const getResponsiveFontSize = (fs: HeadingProps['fontSize']) => {
if (!fs || typeof fs === 'string') return '';
const classes = [];
if (fs.base) classes.push(`text-${fs.base}`);
if (fs.sm) classes.push(`sm:text-${fs.sm}`);
if (fs.md) classes.push(`md:text-${fs.md}`);
if (fs.lg) classes.push(`lg:text-${fs.lg}`);
if (fs.xl) classes.push(`xl:text-${fs.xl}`);
return classes.join(' ');
};
const classes = [
intentClasses[intent as keyof typeof intentClasses] || intentClasses.default,
weightClasses[weight],
fontSize ? getResponsiveFontSize(fontSize) : sizeClasses[level],
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
uppercase ? 'uppercase tracking-widest' : '',
truncate ? 'truncate' : '',
transition ? 'transition-all duration-200' : '',
color?.startsWith('text-') ? color : '',
className,
].join(' ');
const combinedStyle: React.CSSProperties = {
...(mb !== undefined ? { marginBottom: typeof mb === 'number' ? `${mb * 0.25}rem` : mb } : {}),
...(marginBottom !== undefined ? { marginBottom: typeof marginBottom === 'number' ? `${marginBottom * 0.25}rem` : marginBottom } : {}),
...(mt !== undefined ? { marginTop: typeof mt === 'number' ? `${mt * 0.25}rem` : mt } : {}),
...(marginTop !== undefined ? { marginTop: typeof marginTop === 'number' ? `${marginTop * 0.25}rem` : marginTop } : {}),
...(color && !color.startsWith('text-') ? { color } : {}),
...(letterSpacing ? { letterSpacing } : {}),
...(typeof fontSize === 'string' ? { fontSize } : {}),
...(lineHeight ? { lineHeight } : {}),
...(styleProp || {}),
};
return (
<Tag ref={ref} className={classes} style={Object.keys(combinedStyle).length > 0 ? combinedStyle : undefined} id={id}>
<div className="flex items-center gap-2">
{icon}
{children}
</div>
</Tag>
);
});
Heading.displayName = 'Heading';