Files
ermnvldmr.com/packages/ui/src/components/Container/Container.tsx
T
2026-03-07 10:51:54 +03:00

223 lines
4.8 KiB
TypeScript

import { cn } from '@ermnvldmr/stl';
import React, { memo } from 'react';
import { usePress } from 'react-aria';
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
import type { PressEvents } from 'react-aria';
/**
* Tailwind gap spacing scale values.
*/
type GapScale =
| 0
| 0.5
| 1
| 1.5
| 2
| 2.5
| 3
| 3.5
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 14
| 16
| 20
| 24
| 28
| 32
| 36
| 40
| 44
| 48
| 52
| 56
| 60
| 64
| 72
| 80
| 96;
/**
* Background variants for the Container.
*/
export type ContainerBackground =
| 'base'
| 'primary'
| 'secondary'
| 'tertiary'
| 'error'
| 'muted'
| 'transparent';
/**
* Max width constraints for the Container.
*/
export type ContainerMaxWidth = 'sm' | 'md' | 'lg' | 'xl' | 'none';
/**
* Border radius variants for the Container.
*/
export type ContainerRounded = 'none' | 'sm' | 'md' | 'lg' | 'full';
/**
* Props for the Container component.
*/
export interface ContainerProps extends ClassNameProps, TestIdProps {
/** Content to be rendered inside the container */
children: React.ReactNode;
/** Background color variant */
bg?: ContainerBackground;
/** Padding using Tailwind spacing scale */
padding?: GapScale;
/** Maximum width constraint */
maxWidth?: ContainerMaxWidth;
/** Border radius variant */
rounded?: ContainerRounded;
/** Whether to show a shadow */
shadow?: boolean;
/** Whether to show a border */
border?: boolean;
/**
* Press interaction handler.
* Cannot be used with `href`.
*/
onPress?: PressEvents['onPress'];
/**
* If provided, the container will be rendered as an `<a>` tag.
* Cannot be used with `onPress`.
*/
href?: string;
/** The HTML element to use for rendering. Overridden to `a` if `href` is provided. */
as?: React.ElementType;
}
/**
* A generic surface component for creating structural layouts.
*
* Container provides a consistent way to handle backgrounds, padding,
* borders, and width constraints while maintaining theme consistency.
*/
export const Container = memo(function Container({
children,
bg = 'base',
padding = 0,
maxWidth = 'none',
rounded = 'none',
shadow = false,
border = false,
onPress,
href,
as: Component = 'div',
className,
'data-testid': testId,
}: ContainerProps) {
if (process.env.NODE_ENV !== 'production' && onPress && href) {
console.warn('[@ermnvldmr/ui] Container cannot have both `onPress` and `href` props.');
}
const isLink = href != null;
const isPressable = !!onPress && !isLink;
const ref = React.useRef<HTMLElement>(null);
const { pressProps } = usePress({
onPress,
isDisabled: !isPressable,
});
const interactionProps = isPressable ? pressProps : {};
const FinalComponent = isLink ? 'a' : Component;
const bgClasses: Record<ContainerBackground, string> = {
base: 'bg-[var(--rb-container-base)] text-[var(--rb-container-text)]',
primary: 'bg-[var(--rb-primary-base)] text-[var(--rb-primary-text)]',
secondary: 'bg-[var(--rb-secondary-base)] text-[var(--rb-secondary-text)]',
tertiary: 'bg-[var(--rb-tertiary-base)] text-[var(--rb-tertiary-text)]',
error: 'bg-[var(--rb-error-base)] text-[var(--rb-error-text)]',
muted: 'bg-[var(--rb-muted-base)] text-[var(--rb-muted-text)]',
transparent: 'bg-transparent',
};
const maxWidthClasses: Record<ContainerMaxWidth, string> = {
sm: 'max-w-screen-sm',
md: 'max-w-screen-md',
lg: 'max-w-screen-lg',
xl: 'max-w-screen-xl',
none: 'max-w-none',
};
const roundedClasses: Record<ContainerRounded, string> = {
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
full: 'rounded-full',
};
const paddingClasses: Record<GapScale, string> = {
0: 'p-0',
0.5: 'p-0.5',
1: 'p-1',
1.5: 'p-1.5',
2: 'p-2',
2.5: 'p-2.5',
3: 'p-3',
3.5: 'p-3.5',
4: 'p-4',
5: 'p-5',
6: 'p-6',
7: 'p-7',
8: 'p-8',
9: 'p-9',
10: 'p-10',
11: 'p-11',
12: 'p-12',
14: 'p-14',
16: 'p-16',
20: 'p-20',
24: 'p-24',
28: 'p-28',
32: 'p-32',
36: 'p-36',
40: 'p-40',
44: 'p-44',
48: 'p-48',
52: 'p-52',
56: 'p-56',
60: 'p-60',
64: 'p-64',
72: 'p-72',
80: 'p-80',
96: 'p-96',
};
return (
<FinalComponent
{...interactionProps}
ref={ref}
className={cn(
'overflow-hidden',
bgClasses[bg],
maxWidthClasses[maxWidth],
roundedClasses[rounded],
paddingClasses[padding],
shadow && 'shadow-md',
border && 'border border-[var(--rb-outline)]',
(isPressable || isLink) &&
'cursor-pointer transition-all duration-200 hover:brightness-95 active:opacity-80',
className
)}
data-testid={testId}
href={href}
>
{children}
</FinalComponent>
);
});