// Copyright (c) Meta Platforms, Inc. and affiliates. /** * @file Card.tsx * @input Uses container utility, StyleX * @output Exports Card component, CardProps, CardVariant types * @position Core card container component * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Card/Card.doc.mjs (props table, features) * - /packages/core/src/Card/index.ts (exports if types change) * - /apps/storybook/stories/Card.stories.tsx (storybook stories) * - /packages/cli/assets/templates/blocks/components/Card/ (showcase blocks) */ import type { ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import type { StyleXStyles } from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { borderVars, colorVars, radiusVars, shadowVars } from "../theme/tokens.stylex.js"; import { container } from "../Layout/container.stylex.js"; import type { SpacingToken } from "../Layout/container.stylex.js"; import { spacingStepToToken } from "../Layout/padding.stylex.js"; import type { Elevation, SizeValue, SpacingStep } from "../utils/types.js"; import { mergeProps } from "../utils/index.js"; import type { BaseProps } from "../BaseProps.js"; import { themeProps } from "../utils/themeProps.js"; import type { CardVariantMap } from "./index.js"; // ============================================================================= // Variant type // ============================================================================= /** * Background color variant for Card, derived from CardVariantMap. * Extensible via module augmentation of CardVariantMap. * * - `default`: standard card background with visible border * - `transparent`: no background, no visible border — for grouping content without visual weight * - `muted`: subtle muted background for de-emphasised cards * - Non-semantic palette: `blue | cyan | gray | green | orange | pink | purple | red | teal | yellow` * Each uses the corresponding `--color-background-` token (20% opacity tint). * * Only `default` draws a visible border. Its border width is subtracted from * the padding so the total inset (border + padding) equals the padding token — * keeping content geometry faithful to the spacing scale and identical to the * borderless variants. Themes can override borderWidth/borderColor. */ export type CardVariant = keyof CardVariantMap; // ============================================================================= // Styles // ============================================================================= const styles = { card: { "--_card-radius": "x2kkz0m", kaIpWk: "x153u1i6", kVQacm: "x7giv3", kGVxlE: "x1p8z4y0", $$css: true as const }, withBorder: { kMzoRj: "x1litavf", ksu8eU: "x1y0btm7", kVAM5u: "x14i3s5s", kZCmMZ: "xs19ii7", kwRFfy: "x12frdag", kLKAdn: "x1nex4ik", kGO01o: "xbv1mwh", $$css: true as const }, scrollable: { kVQacm: "xysyzu8", $$css: true as const } }; // Background variant styles — each maps to a design token. Typed as PARTIAL // over CardVariant: a theme can add a variant, and the record deliberately has // no entry for it, so the lookup is undefined, StyleX drops it, and the card // takes base styles for the theme rule to paint over. const variantStyles: Partial> = { default: { kWkggS: "x1de1mus", $$css: true as const }, transparent: { kWkggS: "xjbqb8w", $$css: true as const }, muted: { kWkggS: "xwmxj5m", $$css: true as const }, blue: { kWkggS: "x1o0wnni", $$css: true as const }, cyan: { kWkggS: "x1rgj867", $$css: true as const }, gray: { kWkggS: "xspzpui", $$css: true as const }, green: { kWkggS: "x1sqjeoo", $$css: true as const }, orange: { kWkggS: "x1e9xt6e", $$css: true as const }, pink: { kWkggS: "xnpoty2", $$css: true as const }, purple: { kWkggS: "x16i6n6f", $$css: true as const }, red: { kWkggS: "x1cibrc5", $$css: true as const }, teal: { kWkggS: "x1jtji5o", $$css: true as const }, yellow: { kWkggS: "x1bo7t0x", $$css: true as const } }; // Elevation → shadow-token map. Sets the private --_card-elevation variable // (not box-shadow directly) so it composes with --_card-ring in the shadow // list above — a selection ring and a resting elevation coexist without one // overwriting the other. 'none' is a transparent no-op (not the CSS literal // `none`, which would be invalid inside the comma-separated shadow list). const elevationStyles = { none: { "--_card-elevation": "xw28qpl", $$css: true as const }, low: { "--_card-elevation": "x1nn8khe", $$css: true as const }, med: { "--_card-elevation": "x1ovzxg0", $$css: true as const }, high: { "--_card-elevation": "xnpe7fx", $$css: true as const } }; // Dynamic styles for sizing props const dynamicStyles = { sizing: (width: SizeValue | null, height: SizeValue | null, maxWidth: SizeValue | null, minHeight: SizeValue | null) => [{ kzqmXN: width != null ? "x5lhr3w" : width, kZKoxP: height != null ? "x16ye13r" : height, ks0D6T: maxWidth != null ? "xf68679" : maxWidth, kAzted: minHeight != null ? "x82snj4" : minHeight, $$css: true as const }, { "--x-width": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(width), "--x-height": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(height), "--x-maxWidth": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(maxWidth), "--x-minHeight": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(minHeight) }] }; export type { SizeValue } from "../utils/types.js"; export interface CardProps extends BaseProps { ref?: React.Ref; /** * Width of the card. * Numbers are treated as pixels, strings are used as-is. */ width?: SizeValue; /** * Height of the card. * Numbers are treated as pixels, strings are used as-is. */ height?: SizeValue; /** * Maximum width of the card. * Numbers are treated as pixels, strings are used as-is. */ maxWidth?: SizeValue; /** * Minimum height of the card. * Numbers are treated as pixels, strings are used as-is. */ minHeight?: SizeValue; /** * Content to render inside the card. * Should typically be Layout child components. */ children?: ReactNode; /** * Internal padding of the card using the spacing scale. * Accepts numeric spacing steps: 0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10. * * Omit it and the card takes the theme's card padding rather than a step, so * passing a step is a decision to override the theme, not a way to restate * the default. * * @default the theme's card padding (spacing step 4 with no theme) */ padding?: SpacingStep; /** * Background color variant. * - `default`: standard card background with visible border * - `transparent`: no background, no visible border — for grouping without visual weight * - `muted`: subtle muted background for de-emphasised cards * - Non-semantic: `blue`, `cyan`, `gray`, `green`, `orange`, `pink`, `purple`, `red`, `teal`, `yellow` * @default 'default' */ variant?: CardVariant; /** * Resting elevation — the shadow depth the card sits at. * `none` is flat; `low`/`med`/`high` map to the shadow token scale. * @default 'none' */ elevation?: Elevation; } /** * A card container with border and themed styling. * * Applies card-specific appearance (background, border, border-radius) * and sets CSS variables for child layout components. * * @compositionHint Use as a top-level container for elevated content. * Pair with Layout for structured header/content/footer layouts. * * @example * ``` * * Title} * content={Content} * footer={Actions} * /> * * ``` * * @example * ``` * *

Blue tinted card

*
* ``` * * @example * ``` * *

Subtle de-emphasised card

*
* ``` */ export function Card({ width, height, maxWidth, minHeight, children, padding, variant = 'default', elevation = 'none', xstyle, className, style, ref, ...props }: CardProps) { // Only enable scrolling when card has a fixed height (not null/undefined and not "auto") const hasFixedHeight = height != null && height !== 'auto'; // When no explicit padding prop, use theme default (set via container tokens) const useThemeDefault = padding == null; const paddingToken = useThemeDefault ? undefined : spacingStepToToken[padding] as SpacingToken; return
{children}
; } Card.displayName = 'Card';