import type { ScopedColorBg, ScopedColorFg, ScopedColorPalette, ScopedColorStroke, Dimension, Radius, SpacingX, SpacingY, Gradient, Shadow, ScopedColorBanner, } from "@seed-design/css/vars"; import { vars } from "@seed-design/css/vars"; import { forwardRef } from "react"; import { type ResponsiveValue, isResponsiveObject } from "../types/responsive"; export function resolveResponsive( varName: string, value: ResponsiveValue, transform: (v: T) => string | number | undefined, ): Record { if (!isResponsiveObject(value)) { const transformed = transform(value); if (transformed === undefined) return {}; return { [`${varName}-base`]: transformed }; } const result: Record = {}; for (const [breakpoint, breakpointValue] of Object.entries(value)) { if (breakpointValue === undefined) continue; const transformed = transform(breakpointValue); if (transformed === undefined) continue; result[`${varName}-${breakpoint}`] = transformed; } return result; } export function handleColor(color: string | undefined) { if (!color) { return undefined; } const [type, value] = color.split("."); // @ts-expect-error return vars.$color[type]?.[value] ?? color; } export function handleDimension(dimension: string | 0 | undefined): string | undefined { if (dimension == null) { return undefined; } if (typeof dimension === "number") { return `${dimension}px`; } if (dimension === "full") { return "100%"; } const [type, value] = dimension.split("."); // @ts-expect-error return vars.$dimension[dimension] ?? vars.$dimension[type]?.[value] ?? dimension; } function handleBleed( dimension: "asPadding" | string | 0 | undefined, direction: "top" | "right" | "bottom" | "left", ) { if (dimension === "asPadding") { return `var(--seed-box-padding-${direction})`; } return handleDimension(dimension); } function handleShadow(shadow: Shadow | (string & {}) | undefined) { if (!shadow) { return undefined; } // @ts-expect-error return vars.$shadow[shadow] ?? shadow; } function handlePaddingWithSafeArea( padding: string | 0 | undefined, direction: "top" | "bottom", ): string | undefined { if (padding === "safeArea") { return `var(--seed-safe-area-${direction})`; } const paddingValue = handleDimension(padding); return paddingValue; } export function handleRadius(radius: string | 0 | undefined) { if (radius == null) { return undefined; } // @ts-expect-error return vars.$radius[radius] ?? radius; } function handleGradient(gradientToken: string | undefined, direction: string | undefined) { if (!gradientToken || !direction) { return undefined; } // @ts-expect-error const colorStops = vars.$gradient[gradientToken]; if (!colorStops) { return undefined; } return `linear-gradient(${direction}, ${colorStops})`; } interface MarginStyleProps { /** * Margin on all four sides. * * Cannot be combined with any `bleed*` prop. */ margin?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Shorthand for `margin`. * * Cannot be combined with any `bleed*` prop. */ m?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Horizontal margin (left + right). * * Cannot be combined with any `bleed*` prop. */ marginX?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Shorthand for `marginX`. * * Cannot be combined with any `bleed*` prop. */ mx?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Vertical margin (top + bottom). * * Cannot be combined with any `bleed*` prop. */ marginY?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Shorthand for `marginY`. * * Cannot be combined with any `bleed*` prop. */ my?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Top margin. * * Cannot be combined with any `bleed*` prop. */ marginTop?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Shorthand for `marginTop`. * * Cannot be combined with any `bleed*` prop. */ mt?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Right margin. * * Cannot be combined with any `bleed*` prop. */ marginRight?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Shorthand for `marginRight`. * * Cannot be combined with any `bleed*` prop. */ mr?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Bottom margin. * * Cannot be combined with any `bleed*` prop. */ marginBottom?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Shorthand for `marginBottom`. * * Cannot be combined with any `bleed*` prop. */ mb?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Left margin. * * Cannot be combined with any `bleed*` prop. */ marginLeft?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; /** * Shorthand for `marginLeft`. * * Cannot be combined with any `bleed*` prop. */ ml?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "auto" | (string & {}) >; } interface BleedStyleProps { /** * Negative margin on all four sides to extend the element outside its parent. * If set to "asPadding", it will use the padding value in the same direction. * * Cannot be combined with any `margin*` prop. */ bleed?: ResponsiveValue< "asPadding" | Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Negative x-axis margin to extend the element outside its parent. * If set to "asPadding", it will use the padding value in the same direction. * * Cannot be combined with any `margin*` prop. */ bleedX?: ResponsiveValue< "asPadding" | Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Negative y-axis margin to extend the element outside its parent. * If set to "asPadding", it will use the padding value in the same direction. * * Cannot be combined with any `margin*` prop. */ bleedY?: ResponsiveValue< "asPadding" | Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Negative top margin to extend the element outside its parent. * If set to "asPadding", it will use the padding value in the same direction. * * Cannot be combined with any `margin*` prop. */ bleedTop?: ResponsiveValue< "asPadding" | Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Negative right margin to extend the element outside its parent. * If set to "asPadding", it will use the padding value in the same direction. * * Cannot be combined with any `margin*` prop. */ bleedRight?: ResponsiveValue< "asPadding" | Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Negative bottom margin to extend the element outside its parent. * If set to "asPadding", it will use the padding value in the same direction. * * Cannot be combined with any `margin*` prop. */ bleedBottom?: ResponsiveValue< "asPadding" | Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Negative left margin to extend the element outside its parent. * If set to "asPadding", it will use the padding value in the same direction. * * Cannot be combined with any `margin*` prop. */ bleedLeft?: ResponsiveValue< "asPadding" | Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; } /** Distributes `Omit` over each union branch, preserving the union structure. */ export type DistributiveOmit = T extends unknown ? Omit : never; /** * Margin and bleed props both resolve to CSS `margin-*` values, so they * are mutually exclusive at the type level — pick one family, not both. */ export type StyleProps = { /** * Shorthand for `background`. */ bg?: ScopedColorBg | ScopedColorPalette | ScopedColorBanner | (string & {}); background?: ScopedColorBg | ScopedColorPalette | ScopedColorBanner | (string & {}); /** * Shorthand for `backgroundGradient`. */ bgGradient?: Gradient; backgroundGradient?: Gradient; /** * Shorthand for `backgroundGradientDirection`. * e.g. `43deg` */ bgGradientDirection?: | "to right" | "to left" | "to top" | "to bottom" | "to top right" | "to top left" | "to bottom right" | "to bottom left" | (string & {}); /** * e.g. `43deg` */ backgroundGradientDirection?: | "to right" | "to left" | "to top" | "to bottom" | "to top right" | "to top left" | "to bottom right" | "to bottom left" | (string & {}); color?: ScopedColorFg | ScopedColorPalette | (string & {}); borderColor?: ScopedColorStroke | ScopedColorPalette | (string & {}); borderWidth?: 0 | 1 | (string & {}); borderTopWidth?: 0 | 1 | (string & {}); borderRightWidth?: 0 | 1 | (string & {}); borderBottomWidth?: 0 | 1 | (string & {}); borderLeftWidth?: 0 | 1 | (string & {}); borderRadius?: Radius | 0 | (string & {}); borderTopLeftRadius?: Radius | 0 | (string & {}); borderTopRightRadius?: Radius | 0 | (string & {}); borderBottomRightRadius?: Radius | 0 | (string & {}); borderBottomLeftRadius?: Radius | 0 | (string & {}); boxShadow?: Shadow | (string & {}); width?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | "full" | (string & {}) >; minWidth?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | "full" | (string & {}) >; maxWidth?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | "full" | (string & {}) >; height?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | "full" | (string & {}) >; minHeight?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | "full" | (string & {}) >; maxHeight?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | "full" | (string & {}) >; top?: 0 | (string & {}); left?: 0 | (string & {}); right?: 0 | (string & {}); bottom?: 0 | (string & {}); padding?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Shorthand for `padding`. */ p?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; paddingX?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Shorthand for `paddingX`. */ px?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; paddingY?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Shorthand for `paddingY`. */ py?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; paddingTop?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "safeArea" | (string & {}) >; /** * Shorthand for `paddingTop`. */ pt?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "safeArea" | (string & {}) >; paddingRight?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Shorthand for `paddingRight`. */ pr?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; paddingBottom?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "safeArea" | (string & {}) >; /** * Shorthand for `paddingBottom`. */ pb?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | "safeArea" | (string & {}) >; paddingLeft?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; /** * Shorthand for `paddingLeft`. */ pl?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; display?: ResponsiveValue< "block" | "flex" | "grid" | "inline-flex" | "inline" | "inline-block" | "none" >; position?: "relative" | "absolute" | "fixed" | "sticky"; overflowX?: "visible" | "hidden" | "scroll" | "auto"; overflowY?: "visible" | "hidden" | "scroll" | "auto"; zIndex?: number | (string & {}); /** * If true, flex-grow will be set to `1`. */ flexGrow?: 0 | 1 | (number & {}) | true; /** * If true, flex-shrink will be set to `1`. */ flexShrink?: 0 | (number & {}) | true; // Flex flexDirection?: ResponsiveValue<"row" | "column" | "row-reverse" | "column-reverse">; /** * If true, flex-wrap will be set to `wrap`. */ flexWrap?: "wrap" | "wrap-reverse" | "nowrap" | true; justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "space-around"; /** * In flexbox layout, this property is ignored. */ justifySelf?: "center" | "start" | "end" | "stretch"; alignItems?: "flex-start" | "flex-end" | "center" | "stretch"; alignContent?: "flex-start" | "flex-end" | "center" | "stretch"; alignSelf?: "flex-start" | "flex-end" | "center" | "stretch"; gap?: ResponsiveValue< Dimension | `spacingX.${SpacingX}` | `spacingY.${SpacingY}` | 0 | (string & {}) >; // Grid Item // NOTE: gridArea는 지원하지 않습니다. // grid-area가 grid-column/row의 shorthand이므로 CSS 변수로 동시에 바인딩하면 충돌합니다. gridColumn?: string; gridRow?: string; // NOTE: Not sure how to treat transform/translate right now, mark as unstable until we have a better solution. unstable_transform?: string; _active?: { bg?: ScopedColorBg | ScopedColorPalette | (string & {}); background?: ScopedColorBg | ScopedColorPalette | (string & {}); }; } & ( | (BleedStyleProps & { [K in keyof MarginStyleProps]?: never }) | (MarginStyleProps & { [K in keyof BleedStyleProps]?: never }) ); type UseStyleProps = StyleProps & { style?: React.CSSProperties; }; export function useStyleProps( props: T, ): { style: React.CSSProperties; restProps: Omit; } { const { background, bg, bgGradient, backgroundGradient, bgGradientDirection, backgroundGradientDirection, color, borderColor, borderWidth, borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth, borderRadius, borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius, boxShadow, width, minWidth, maxWidth, height, minHeight, maxHeight, padding, paddingX, paddingY, paddingTop, paddingRight, paddingBottom, paddingLeft, p, px, py, pt, pr, pb, pl, bleed, bleedX, bleedY, bleedTop, bleedRight, bleedBottom, bleedLeft, margin, m, marginX, mx, marginY, my, marginTop, mt, marginRight, mr, marginBottom, mb, marginLeft, ml, bottom, left, right, top, display, position, overflowX, overflowY, zIndex, flexGrow, flexShrink, flexDirection, flexWrap, justifyContent, justifySelf, alignItems, alignContent, alignSelf, gap, gridColumn, gridRow, unstable_transform, _active, style, ...restProps } = props; const gradientValue = handleGradient( bgGradient ?? backgroundGradient, bgGradientDirection ?? backgroundGradientDirection, ); return { style: { // Non-responsive props "--seed-box-background": handleColor(background ?? bg) ?? gradientValue, "--seed-box-color": handleColor(color), "--seed-box-border-color": handleColor(borderColor), "--seed-box-border-width": borderWidth !== undefined ? `${borderWidth}px` : undefined, "--seed-box-border-top-width": borderTopWidth !== undefined ? `${borderTopWidth}px` : undefined, "--seed-box-border-right-width": borderRightWidth !== undefined ? `${borderRightWidth}px` : undefined, "--seed-box-border-bottom-width": borderBottomWidth !== undefined ? `${borderBottomWidth}px` : undefined, "--seed-box-border-left-width": borderLeftWidth !== undefined ? `${borderLeftWidth}px` : undefined, "--seed-box-border-radius": handleRadius(borderRadius), "--seed-box-border-top-left-radius": handleRadius(borderTopLeftRadius), "--seed-box-border-top-right-radius": handleRadius(borderTopRightRadius), "--seed-box-border-bottom-right-radius": handleRadius(borderBottomRightRadius), "--seed-box-border-bottom-left-radius": handleRadius(borderBottomLeftRadius), "--seed-box-box-shadow": handleShadow(boxShadow), "--seed-box-top": top, "--seed-box-left": left, "--seed-box-right": right, "--seed-box-bottom": bottom, "--seed-box-position": position, "--seed-box-overflow-x": overflowX, "--seed-box-overflow-y": overflowY, "--seed-box-z-index": zIndex, "--seed-box-flex-grow": flexGrow === true ? 1 : flexGrow, "--seed-box-flex-shrink": flexShrink === true ? 1 : flexShrink, "--seed-box-flex-wrap": flexWrap === true ? "wrap" : flexWrap, "--seed-box-justify-content": justifyContent, "--seed-box-justify-self": justifySelf, "--seed-box-align-items": alignItems, "--seed-box-align-content": alignContent, "--seed-box-align-self": alignSelf, "--seed-box-grid-column": gridColumn, "--seed-box-grid-row": gridRow, "--seed-box-unstable-transform": unstable_transform, // Active "--seed-box-background--active": handleColor(_active?.bg ?? _active?.background), // Responsive props (breakpoint-suffixed) ...(width !== undefined && resolveResponsive("--seed-box-width", width, handleDimension)), ...(minWidth !== undefined && resolveResponsive("--seed-box-min-width", minWidth, handleDimension)), ...(maxWidth !== undefined && resolveResponsive("--seed-box-max-width", maxWidth, handleDimension)), ...(height !== undefined && resolveResponsive("--seed-box-height", height, handleDimension)), ...(minHeight !== undefined && resolveResponsive("--seed-box-min-height", minHeight, handleDimension)), ...(maxHeight !== undefined && resolveResponsive("--seed-box-max-height", maxHeight, handleDimension)), ...((padding ?? p) !== undefined && resolveResponsive("--seed-box-padding", padding ?? p, handleDimension)), ...((paddingX ?? px) !== undefined && resolveResponsive("--seed-box-padding-x", paddingX ?? px, handleDimension)), ...((paddingY ?? py) !== undefined && resolveResponsive("--seed-box-padding-y", paddingY ?? py, handleDimension)), ...((paddingTop ?? pt) !== undefined && resolveResponsive("--seed-box-padding-top", paddingTop ?? pt, (v) => handlePaddingWithSafeArea(v, "top"), )), ...((paddingRight ?? pr) !== undefined && resolveResponsive("--seed-box-padding-right", paddingRight ?? pr, handleDimension)), ...((paddingBottom ?? pb) !== undefined && resolveResponsive("--seed-box-padding-bottom", paddingBottom ?? pb, (v) => handlePaddingWithSafeArea(v, "bottom"), )), ...((paddingLeft ?? pl) !== undefined && resolveResponsive("--seed-box-padding-left", paddingLeft ?? pl, handleDimension)), ...((bleedTop ?? bleedY ?? bleed) !== undefined && resolveResponsive("--seed-box-bleed-top", bleedTop ?? bleedY ?? bleed, (v) => handleBleed(v, "top"), )), ...((bleedRight ?? bleedX ?? bleed) !== undefined && resolveResponsive("--seed-box-bleed-right", bleedRight ?? bleedX ?? bleed, (v) => handleBleed(v, "right"), )), ...((bleedBottom ?? bleedY ?? bleed) !== undefined && resolveResponsive("--seed-box-bleed-bottom", bleedBottom ?? bleedY ?? bleed, (v) => handleBleed(v, "bottom"), )), ...((bleedLeft ?? bleedX ?? bleed) !== undefined && resolveResponsive("--seed-box-bleed-left", bleedLeft ?? bleedX ?? bleed, (v) => handleBleed(v, "left"), )), ...((margin ?? m) !== undefined && resolveResponsive("--seed-box-margin", margin ?? m, handleDimension)), ...((marginX ?? mx) !== undefined && resolveResponsive("--seed-box-margin-x", marginX ?? mx, handleDimension)), ...((marginY ?? my) !== undefined && resolveResponsive("--seed-box-margin-y", marginY ?? my, handleDimension)), ...((marginTop ?? mt) !== undefined && resolveResponsive("--seed-box-margin-top", marginTop ?? mt, handleDimension)), ...((marginRight ?? mr) !== undefined && resolveResponsive("--seed-box-margin-right", marginRight ?? mr, handleDimension)), ...((marginBottom ?? mb) !== undefined && resolveResponsive("--seed-box-margin-bottom", marginBottom ?? mb, handleDimension)), ...((marginLeft ?? ml) !== undefined && resolveResponsive("--seed-box-margin-left", marginLeft ?? ml, handleDimension)), ...(gap !== undefined && resolveResponsive("--seed-box-gap", gap, handleDimension)), ...(display !== undefined && resolveResponsive("--seed-box-display", display, (v) => v)), ...(flexDirection !== undefined && resolveResponsive("--seed-box-flex-direction", flexDirection, (v) => v)), ...style, } as React.CSSProperties, restProps: { ...restProps, // see global.ts to understand why we need this ...((_active?.bg != null || _active?.background != null) && { "data-has-active-bg": "" }), }, }; } export function withStyleProps

( Component: React.ComponentType

>, ) { const Node = forwardRef((props, ref) => { const { style, restProps } = useStyleProps(props); // @ts-expect-error return ; }); Node.displayName = Component.displayName || Component.name; return Node; }