From 489deb2991e1653a246fac20d3464d1f12c8d655 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 18 Jan 2026 16:57:16 +0100 Subject: [PATCH] website refactor --- apps/website/ui/Layout.tsx | 70 ++--- apps/website/ui/primitives/Box.tsx | 360 ++++-------------------- apps/website/ui/primitives/Grid.tsx | 33 ++- apps/website/ui/primitives/GridItem.tsx | 11 + apps/website/ui/primitives/Stack.tsx | 93 +++++- apps/website/ui/primitives/Surface.tsx | 25 +- 6 files changed, 233 insertions(+), 359 deletions(-) diff --git a/apps/website/ui/Layout.tsx b/apps/website/ui/Layout.tsx index 16efd5296..0469c12af 100644 --- a/apps/website/ui/Layout.tsx +++ b/apps/website/ui/Layout.tsx @@ -1,4 +1,16 @@ import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; +import { Grid } from './primitives/Grid'; +import { Stack } from './primitives/Stack'; + +/** + * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS COMPONENT. + * + * Layout is a high-level component for page or section layouts. + * It should use Grid or Stack primitives internally. + * + * If you need a specific layout pattern, create a new component. + */ interface LayoutProps { children: ReactNode; @@ -25,39 +37,33 @@ export function Layout({ items = 'start', justify = 'start' }: LayoutProps) { - const baseClasses = [padding, gap, className]; - if (grid) { - const gridColsMap = { - 1: 'grid-cols-1', - 2: 'grid-cols-2', - 3: 'grid-cols-3', - 4: 'grid-cols-4' - }; - baseClasses.push('grid', gridColsMap[gridCols]); - } else if (flex) { - baseClasses.push('flex'); - if (flexCol) baseClasses.push('flex-col'); - - const itemsMap = { - start: 'items-start', - center: 'items-center', - end: 'items-end', - stretch: 'items-stretch' - }; - baseClasses.push(itemsMap[items]); - - const justifyMap = { - start: 'justify-start', - center: 'justify-center', - end: 'justify-end', - between: 'justify-between', - around: 'justify-around' - }; - baseClasses.push(justifyMap[justify]); + return ( + + {children} + + ); } - const classes = baseClasses.filter(Boolean).join(' '); + if (flex) { + return ( + + {children} + + ); + } - return
{children}
; -} \ No newline at end of file + return ( + + {children} + + ); +} diff --git a/apps/website/ui/primitives/Box.tsx b/apps/website/ui/primitives/Box.tsx index c66943b3b..5a1701e47 100644 --- a/apps/website/ui/primitives/Box.tsx +++ b/apps/website/ui/primitives/Box.tsx @@ -1,5 +1,17 @@ import React, { forwardRef, ForwardedRef, ElementType, ComponentPropsWithoutRef } from 'react'; +/** + * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. + * + * Box is a basic container primitive for spacing, sizing and basic styling. + * + * - DO NOT add layout props (flex, grid, gap) - use Stack or Grid instead. + * - DO NOT add positioning props (absolute, top, zIndex) - create a specific component. + * - DO NOT add animation props - create a specific component. + * + * If you need more complex behavior, create a specific component in apps/website/components. + */ + type Spacing = 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; interface ResponsiveSpacing { @@ -8,13 +20,20 @@ interface ResponsiveSpacing { lg?: Spacing; } +export type ResponsiveValue = { + base?: T; + sm?: T; + md?: T; + lg?: T; + xl?: T; + '2xl'?: T; +}; + export interface BoxProps { as?: T; children?: React.ReactNode; className?: string; - center?: boolean; - fullWidth?: boolean; - fullHeight?: boolean; + // Spacing m?: Spacing | ResponsiveSpacing; mt?: Spacing | ResponsiveSpacing; mb?: Spacing | ResponsiveSpacing; @@ -29,220 +48,70 @@ export interface BoxProps { pr?: Spacing | ResponsiveSpacing; px?: Spacing | ResponsiveSpacing; py?: Spacing | ResponsiveSpacing; - display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none'>; - flexDirection?: 'row' | 'row-reverse' | 'col' | 'col-reverse' | ResponsiveValue<'row' | 'row-reverse' | 'col' | 'col-reverse'>; - alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>; - justifyContent?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'>; - flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse'; - flexShrink?: number; - flexGrow?: number; - flex?: number | string; - alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline'; - gridCols?: number | string; - responsiveGridCols?: { - base?: number | string; - md?: number | string; - lg?: number | string; - }; - gap?: Spacing | ResponsiveSpacing; - colSpan?: number | string; - responsiveColSpan?: { - base?: number | string; - md?: number | string; - lg?: number | string; - }; - position?: 'relative' | 'absolute' | 'fixed' | 'sticky'; - top?: Spacing | string; - bottom?: Spacing | string; - left?: Spacing | string; - right?: Spacing | string; - overflow?: 'visible' | 'hidden' | 'scroll' | 'auto'; - maxWidth?: string | ResponsiveValue; - minWidth?: string | ResponsiveValue; - maxHeight?: string | ResponsiveValue; - minHeight?: string | ResponsiveValue; - zIndex?: number; + // Sizing w?: string | ResponsiveValue; h?: string | ResponsiveValue; width?: string; height?: string; + maxWidth?: string | ResponsiveValue; + minWidth?: string | ResponsiveValue; + maxHeight?: string | ResponsiveValue; + minHeight?: string | ResponsiveValue; + // Display + display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none'>; + // Basic Styling rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'; border?: boolean; - borderTop?: boolean; - borderBottom?: boolean; - borderLeft?: boolean; - borderRight?: boolean; borderColor?: string; bg?: string; color?: string; shadow?: string; - hoverBorderColor?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transition?: any; - cursor?: 'pointer' | 'default' | 'wait' | 'text' | 'move' | 'not-allowed'; - lineClamp?: 1 | 2 | 3 | 4 | 5 | 6; - inset?: string; - bgOpacity?: number; opacity?: number; - blur?: 'none' | 'sm' | 'md' | 'lg' | 'xl'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - animate?: any; - translateX?: string; - textAlign?: 'left' | 'center' | 'right' | 'justify'; - hoverScale?: boolean; - group?: boolean; - groupHoverBorderColor?: string; - groupHoverTextColor?: string; - groupHoverScale?: boolean; - groupHoverOpacity?: number; - groupHoverWidth?: 'full' | '0' | 'auto'; - fontSize?: string; - fontWeight?: 'normal' | 'medium' | 'semibold' | 'bold'; - transform?: string; - borderWidth?: string; - hoverTextColor?: string; - hoverBg?: string; - borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none'; - ring?: string; - objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; - aspectRatio?: string; - visibility?: 'visible' | 'hidden' | 'collapse'; - pointerEvents?: 'auto' | 'none'; + // Flex/Grid Item props + flex?: number | string; + flexShrink?: number; + flexGrow?: number; + alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline'; order?: number | string | ResponsiveValue; - hideScrollbar?: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - whileHover?: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - whileTap?: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - initial?: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - exit?: any; + // Events onMouseEnter?: React.MouseEventHandler; onMouseLeave?: React.MouseEventHandler; onClick?: React.MouseEventHandler; - onSubmit?: React.FormEventHandler; style?: React.CSSProperties; - hoverColor?: string; - maskImage?: string; - webkitMaskImage?: string; - backgroundSize?: string; - backgroundPosition?: string; - backgroundColor?: string; - insetY?: Spacing | string; - letterSpacing?: string; - lineHeight?: string; - backgroundImage?: string; + id?: string; + role?: string; + tabIndex?: number; } -export type ResponsiveValue = { - base?: T; - sm?: T; - md?: T; - lg?: T; - xl?: T; - '2xl'?: T; -}; - export const Box = forwardRef(( { as, children, className = '', - center = false, - fullWidth = false, - fullHeight = false, m, mt, mb, ml, mr, mx, my, p, pt, pb, pl, pr, px, py, + w, h, width, height, + maxWidth, minWidth, maxHeight, minHeight, display, - flexDirection, - alignItems, - justifyContent, - flexWrap, - position, - top, - bottom, - left, - right, - overflow, - maxWidth, - minWidth, - maxHeight, - minHeight, - zIndex, - gridCols, - responsiveGridCols, - colSpan, - responsiveColSpan, - gap, - w, - h, - width, - height, rounded, border, - borderTop, - borderBottom, - borderLeft, - borderRight, borderColor, bg, color, shadow, + opacity, flex, flexShrink, flexGrow, alignSelf, - hoverBorderColor, - transition, - cursor, - lineClamp, - inset, - bgOpacity, - opacity, - blur, - animate, - translateX, - textAlign, - hoverScale, - group, - groupHoverBorderColor, - groupHoverTextColor, - groupHoverScale, - groupHoverOpacity, - groupHoverWidth, - fontSize, - fontWeight, - transform, - borderWidth, - hoverTextColor, - hoverBg, - borderStyle, - ring, - objectFit, - aspectRatio, - visibility, - pointerEvents, order, - hideScrollbar, - whileHover, - whileTap, - initial, - exit, onMouseEnter, onMouseLeave, onClick, - onSubmit, - hoverColor, - maskImage, - webkitMaskImage, - backgroundSize, - backgroundPosition, - backgroundColor, - insetY, - letterSpacing, - lineHeight, - backgroundImage, + style: styleProp, + id, + role, + tabIndex, ...props }: BoxProps & ComponentPropsWithoutRef, ref: ForwardedRef @@ -284,57 +153,7 @@ export const Box = forwardRef(( return prefix ? `${prefix}-${value}` : String(value); }; - const getFlexDirectionClass = (value: BoxProps['flexDirection']) => { - if (!value) return ''; - if (typeof value === 'object') { - const classes = []; - if (value.base) classes.push(`flex-${value.base}`); - if (value.sm) classes.push(`sm:flex-${value.sm}`); - if (value.md) classes.push(`md:flex-${value.md}`); - if (value.lg) classes.push(`lg:flex-${value.lg}`); - if (value.xl) classes.push(`xl:flex-${value.xl}`); - if (value['2xl']) classes.push(`2xl:flex-${value['2xl']}`); - return classes.join(' '); - } - return `flex-${value}`; - }; - - const getAlignItemsClass = (value: BoxProps['alignItems']) => { - if (!value) return ''; - const map: Record = { start: 'items-start', center: 'items-center', end: 'items-end', stretch: 'items-stretch', baseline: 'items-baseline' }; - if (typeof value === 'object') { - const classes = []; - if (value.base) classes.push(map[value.base]); - if (value.sm) classes.push(`sm:${map[value.sm]}`); - if (value.md) classes.push(`md:${map[value.md]}`); - if (value.lg) classes.push(`lg:${map[value.lg]}`); - if (value.xl) classes.push(`xl:${map[value.xl]}`); - if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`); - return classes.join(' '); - } - return map[value]; - }; - - const getJustifyContentClass = (value: BoxProps['justifyContent']) => { - if (!value) return ''; - const map: Record = { start: 'justify-start', center: 'justify-center', end: 'justify-end', between: 'justify-between', around: 'justify-around', evenly: 'justify-evenly' }; - if (typeof value === 'object') { - const classes = []; - if (value.base) classes.push(map[value.base]); - if (value.sm) classes.push(`sm:${map[value.sm]}`); - if (value.md) classes.push(`md:${map[value.md]}`); - if (value.lg) classes.push(`lg:${map[value.lg]}`); - if (value.xl) classes.push(`xl:${map[value.xl]}`); - if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`); - return classes.join(' '); - } - return map[value]; - }; - const classes = [ - center ? 'flex items-center justify-center' : '', - fullWidth ? 'w-full' : '', - fullHeight ? 'h-full' : '', getSpacingClass('m', m), getSpacingClass('mt', mt), getSpacingClass('mb', mb), @@ -355,71 +174,19 @@ export const Box = forwardRef(( getResponsiveClasses('min-w', minWidth), getResponsiveClasses('max-h', maxHeight), getResponsiveClasses('min-h', minHeight), + getResponsiveClasses('', display), rounded ? `rounded-${rounded}` : '', border ? 'border' : '', - borderStyle ? `border-${borderStyle}` : '', - borderTop ? 'border-t' : '', - borderBottom ? 'border-b' : '', - borderLeft ? 'border-l' : '', - borderRight ? 'border-r' : '', borderColor ? borderColor : '', - ring ? ring : '', - bg ? bg : (backgroundColor ? (backgroundColor.startsWith('bg-') ? backgroundColor : `bg-${backgroundColor}`) : ''), + bg ? bg : '', color ? color : '', - hoverColor ? `hover:${hoverColor}` : '', shadow ? shadow : '', flex !== undefined ? `flex-${flex}` : '', flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '', flexGrow !== undefined ? `flex-grow-${flexGrow}` : '', alignSelf !== undefined ? `self-${alignSelf}` : '', - flexWrap ? `flex-${flexWrap}` : '', - hoverBorderColor ? `hover:${hoverBorderColor}` : '', - hoverTextColor ? `hover:${hoverTextColor}` : '', - hoverBg ? `hover:${hoverBg}` : '', - transition ? 'transition-all' : '', - lineClamp ? `line-clamp-${lineClamp}` : '', - inset ? `inset-${inset}` : '', - insetY !== undefined && spacingMap[insetY as string | number] ? `inset-y-${spacingMap[insetY as string | number]}` : '', - bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '', opacity !== undefined ? `opacity-${opacity * 100}` : '', - blur ? `blur-${blur}` : '', - animate ? `animate-${animate}` : '', - translateX ? `translate-x-${translateX}` : '', - textAlign ? `text-${textAlign}` : '', - hoverScale ? 'hover:scale-[1.02]' : '', - group ? 'group' : '', - groupHoverBorderColor ? `group-hover:border-${groupHoverBorderColor}` : '', - groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '', - groupHoverScale ? 'group-hover:scale-[1.02]' : '', - groupHoverOpacity !== undefined ? `group-hover:opacity-${groupHoverOpacity * 100}` : '', - groupHoverWidth ? `group-hover:w-${groupHoverWidth}` : '', - fontSize ? `text-${fontSize}` : '', - fontWeight ? `font-${fontWeight}` : '', - getResponsiveClasses('', display), - getFlexDirectionClass(flexDirection), - getAlignItemsClass(alignItems), - getJustifyContentClass(justifyContent), - gridCols ? `grid-cols-${gridCols}` : '', - responsiveGridCols?.base ? `grid-cols-${responsiveGridCols.base}` : '', - responsiveGridCols?.md ? `md:grid-cols-${responsiveGridCols.md}` : '', - responsiveGridCols?.lg ? `lg:grid-cols-${responsiveGridCols.lg}` : '', - colSpan ? `col-span-${colSpan}` : '', - responsiveColSpan?.base ? `col-span-${responsiveColSpan.base}` : '', - responsiveColSpan?.md ? `md:col-span-${responsiveColSpan.md}` : '', - responsiveColSpan?.lg ? `lg:col-span-${responsiveColSpan.lg}` : '', - getSpacingClass('gap', gap), getResponsiveClasses('order', order), - position ? position : '', - top !== undefined && spacingMap[top as string | number] ? `top-${spacingMap[top as string | number]}` : '', - bottom !== undefined && spacingMap[bottom as string | number] ? `bottom-${spacingMap[bottom as string | number]}` : '', - left !== undefined && spacingMap[left as string | number] ? `left-${spacingMap[left as string | number]}` : '', - right !== undefined && spacingMap[right as string | number] ? `right-${spacingMap[right as string | number]}` : '', - overflow ? `overflow-${overflow}` : '', - visibility ? visibility : '', - zIndex !== undefined ? `z-${zIndex}` : '', - cursor ? `cursor-${cursor}` : '', - pointerEvents ? `pointer-events-${pointerEvents}` : '', - hideScrollbar ? 'scrollbar-hide' : '', className ].filter(Boolean).join(' '); @@ -430,25 +197,7 @@ export const Box = forwardRef(( ...(typeof minWidth === 'string' ? { minWidth } : {}), ...(typeof maxHeight === 'string' ? { maxHeight } : {}), ...(typeof minHeight === 'string' ? { minHeight } : {}), - ...(fontSize ? { fontSize } : {}), - ...(transform ? { transform } : {}), - ...(borderWidth ? { borderWidth } : {}), - ...(objectFit ? { objectFit } : {}), - ...(aspectRatio ? { aspectRatio } : {}), - ...(maskImage ? { maskImage } : {}), - ...(webkitMaskImage ? { WebkitMaskImage: webkitMaskImage } : {}), - ...(backgroundSize ? { backgroundSize } : {}), - ...(backgroundPosition ? { backgroundPosition } : {}), - ...(backgroundImage ? { backgroundImage } : {}), - ...(letterSpacing ? { letterSpacing } : {}), - ...(lineHeight ? { lineHeight } : {}), - ...(top !== undefined && !spacingMap[top as string | number] ? { top } : {}), - ...(bottom !== undefined && !spacingMap[bottom as string | number] ? { bottom } : {}), - ...(left !== undefined && !spacingMap[left as string | number] ? { left } : {}), - ...(right !== undefined && !spacingMap[right as string | number] ? { right } : {}), - ...(insetY !== undefined && !spacingMap[insetY as string | number] ? { top: insetY, bottom: insetY } : {}), - ...(hideScrollbar ? { scrollbarWidth: 'none', msOverflowStyle: 'none', '&::-webkit-scrollbar': { display: 'none' } } : {}), - ...((props as Record).style as object || {}) + ...(styleProp || {}) }; return ( @@ -456,16 +205,13 @@ export const Box = forwardRef(( ref={ref as React.ForwardedRef} className={classes} onClick={onClick} - onSubmit={onSubmit} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} - whileHover={whileHover} - whileTap={whileTap} - initial={initial} - animate={animate} - exit={exit} + style={style} + id={id} + role={role} + tabIndex={tabIndex} {...props} - style={style as React.CSSProperties} > {children} diff --git a/apps/website/ui/primitives/Grid.tsx b/apps/website/ui/primitives/Grid.tsx index bc50c04c7..02ea4780e 100644 --- a/apps/website/ui/primitives/Grid.tsx +++ b/apps/website/ui/primitives/Grid.tsx @@ -1,12 +1,41 @@ import React, { ReactNode } from 'react'; -import { Box, BoxProps } from './Box'; +import { Box, BoxProps, ResponsiveValue } from './Box'; -export interface GridProps extends Omit, 'children' | 'display'> { +/** + * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. + * + * Grid is for CSS Grid-based layouts. + * + * - DO NOT add positioning props (absolute, top, zIndex). + * - DO NOT add flex props. + * - DO NOT add background/border props unless it's a specific styled grid. + * + * If you need a more specific layout, create a new component in apps/website/components. + */ + +export interface GridProps { children: ReactNode; cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12; mdCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12; lgCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12; gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16; + className?: string; + // Spacing + m?: number; + mt?: number; + mb?: number; + ml?: number; + mr?: number; + p?: number; + pt?: number; + pb?: number; + pl?: number; + pr?: number; + px?: number; + py?: number; + // Sizing + w?: string | ResponsiveValue; + h?: string | ResponsiveValue; } export function Grid({ diff --git a/apps/website/ui/primitives/GridItem.tsx b/apps/website/ui/primitives/GridItem.tsx index b19be16b0..cb0809c69 100644 --- a/apps/website/ui/primitives/GridItem.tsx +++ b/apps/website/ui/primitives/GridItem.tsx @@ -1,6 +1,17 @@ import React from 'react'; import { Box } from './Box'; +/** + * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. + * + * GridItem is for items inside a Grid container. + * + * - DO NOT add positioning props (absolute, top, zIndex). + * - DO NOT add background/border props. + * + * If you need a more specific layout, create a new component in apps/website/components. + */ + export interface GridItemProps { children: React.ReactNode; colSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; diff --git a/apps/website/ui/primitives/Stack.tsx b/apps/website/ui/primitives/Stack.tsx index 36f9866ba..ff0db3bf2 100644 --- a/apps/website/ui/primitives/Stack.tsx +++ b/apps/website/ui/primitives/Stack.tsx @@ -1,6 +1,18 @@ import React, { ReactNode, ElementType } from 'react'; import { Box, BoxProps, ResponsiveValue } from './Box'; +/** + * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. + * + * Stack is for flexbox-based layouts (stacking elements). + * + * - DO NOT add positioning props (absolute, top, zIndex). + * - DO NOT add grid props. + * - DO NOT add background/border props unless it's a specific styled stack. + * + * If you need a more specific layout, create a new component in apps/website/components. + */ + type Spacing = 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; interface ResponsiveGap { @@ -20,7 +32,8 @@ interface ResponsiveSpacing { '2xl'?: Spacing; } -export interface StackProps extends Omit, 'children' | 'className' | 'gap'> { +export interface StackProps { + as?: T; children: ReactNode; className?: string; direction?: 'row' | 'col' | { base?: 'row' | 'col'; md?: 'row' | 'col'; lg?: 'row' | 'col' }; @@ -28,7 +41,7 @@ export interface StackProps extends Omit, 'ch align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>; justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>; wrap?: boolean; - center?: boolean; + // Spacing (allowed for layout) m?: Spacing | ResponsiveSpacing; mt?: Spacing | ResponsiveSpacing; mb?: Spacing | ResponsiveSpacing; @@ -41,11 +54,21 @@ export interface StackProps extends Omit, 'ch pr?: Spacing | ResponsiveSpacing; px?: Spacing | ResponsiveSpacing; py?: Spacing | ResponsiveSpacing; + // Sizing (allowed for layout) + w?: string | ResponsiveValue; + h?: string | ResponsiveValue; + minWidth?: string | ResponsiveValue; + maxWidth?: string | ResponsiveValue; + minHeight?: string | ResponsiveValue; + maxHeight?: string | ResponsiveValue; + // Basic styling (sometimes needed for containers) rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'; + // Flex item props + flex?: number | string; + flexGrow?: number; + flexShrink?: number; + alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline'; style?: React.CSSProperties; - role?: string; - 'aria-label'?: string; - 'aria-live'?: 'polite' | 'assertive' | 'off'; } export function Stack({ @@ -56,10 +79,14 @@ export function Stack({ align, justify, wrap = false, - center = false, m, mt, mb, ml, mr, p, pt, pb, pl, pr, px, py, + w, h, minWidth, maxWidth, minHeight, maxHeight, rounded, + flex, + flexGrow, + flexShrink, + alignSelf, as, ...props }: StackProps) { @@ -150,16 +177,58 @@ export function Stack({ className ].filter(Boolean).join(' '); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { alignItems, justifyContent, ...restProps } = props as any; + const getAlignItemsClass = (value: StackProps['align']) => { + if (!value) return ''; + const map: Record = { start: 'items-start', center: 'items-center', end: 'items-end', stretch: 'items-stretch', baseline: 'items-baseline' }; + if (typeof value === 'object') { + const classes = []; + if (value.base) classes.push(map[value.base]); + if (value.sm) classes.push(`sm:${map[value.sm]}`); + if (value.md) classes.push(`md:${map[value.md]}`); + if (value.lg) classes.push(`lg:${map[value.lg]}`); + if (value.xl) classes.push(`xl:${map[value.xl]}`); + if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`); + return classes.join(' '); + } + return map[value]; + }; + + const getJustifyContentClass = (value: StackProps['justify']) => { + if (!value) return ''; + const map: Record = { start: 'justify-start', center: 'justify-center', end: 'justify-end', between: 'justify-between', around: 'justify-around' }; + if (typeof value === 'object') { + const classes = []; + if (value.base) classes.push(map[value.base]); + if (value.sm) classes.push(`sm:${map[value.sm]}`); + if (value.md) classes.push(`md:${map[value.md]}`); + if (value.lg) classes.push(`lg:${map[value.lg]}`); + if (value.xl) classes.push(`xl:${map[value.xl]}`); + if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`); + return classes.join(' '); + } + return map[value]; + }; + + const layoutClasses = [ + getAlignItemsClass(align), + getJustifyContentClass(justify) + ].filter(Boolean).join(' '); return ( {children} diff --git a/apps/website/ui/primitives/Surface.tsx b/apps/website/ui/primitives/Surface.tsx index 973b82335..37c2e4e44 100644 --- a/apps/website/ui/primitives/Surface.tsx +++ b/apps/website/ui/primitives/Surface.tsx @@ -1,7 +1,18 @@ import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react'; -import { Box, BoxProps } from './Box'; +import { Box, BoxProps, ResponsiveValue } from './Box'; -export interface SurfaceProps extends Omit, 'children' | 'className' | 'display'> { +/** + * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. + * + * Surface is a styled container with background, border, and shadow. + * + * - DO NOT add layout props (flex, grid, gap) - use Stack or Grid instead. + * - DO NOT add positioning props (absolute, top, zIndex). + * + * If you need a more specific layout, create a new component in apps/website/components. + */ + +export interface SurfaceProps { as?: T; children: ReactNode; variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner'; @@ -9,8 +20,11 @@ export interface SurfaceProps extends Omit; + h?: string | ResponsiveValue; + maxWidth?: string | ResponsiveValue; } export function Surface({ @@ -21,8 +35,8 @@ export function Surface({ border = false, padding = 0, className = '', - display, shadow = 'none', + w, h, maxWidth, ...props }: SurfaceProps & ComponentPropsWithoutRef) { const variantClasses: Record = { @@ -75,12 +89,11 @@ export function Surface({ border ? 'border border-border-gray' : '', paddingClasses[padding] || 'p-0', shadowClasses[shadow], - display ? display : '', className ].filter(Boolean).join(' '); return ( - + {children} );