Info Card

Sidebar bottom announcement / upgrade card (lg warning, sm trial+progress).

Installation

$npx @309-thingspire/ui@latest add info-card

Usage

import { InfoCard } from "@/components/info-card/info-card"
<InfoCard />

Examples

Live preview rendered from InfoCard.preview.tsx. Switch to the Code tab to view the underlying component source.

Loading preview…
import React from 'react';

import { colors, radius, spacing, typography } from '../../style-tokens';
import { Button } from '../Button/Button';
import { IconAlertLine, IconCloseLine } from '../icons';
import { ProgressBar } from '../ProgressBar/ProgressBar';

import type { InfoCardProps } from './InfoCard.types';

const palette = colors.primitive.palette;

const SIDEBAR_WIDTH = spacing.primitive['256'] + spacing.scale['24'];
const PROGRESS_BAR_WIDTH = spacing.primitive['224'] + spacing.scale['24'];

const labelTypography = typography.scale.captionL.medium;
const captionTypography = typography.scale.captionL.regular;

const labelTextStyle: React.CSSProperties = {
  fontFamily: labelTypography.fontFamily,
  fontSize: labelTypography.fontSize,
  fontWeight: labelTypography.fontWeight,
  lineHeight: `${labelTypography.lineHeight}px`,
  letterSpacing: `${labelTypography.letterSpacing}px`,
  color: palette.gray['13'],
  margin: 0,
};

const captionTextStyle: React.CSSProperties = {
  fontFamily: captionTypography.fontFamily,
  fontSize: captionTypography.fontSize,
  fontWeight: captionTypography.fontWeight,
  lineHeight: `${captionTypography.lineHeight}px`,
  letterSpacing: `${captionTypography.letterSpacing}px`,
  color: palette.gray['9a'],
  margin: 0,
};

export function InfoCard({
  type = 'lg',
  label = 'Label',
  caption = 'A caption is the words printed underneath a picture or cartoon which explain what it is about.',
  leadIcon,
  primaryButtonLabel = 'Button',
  secondaryButtonLabel = 'Button',
  showCloseButton = true,
  progressLabel = 'Free trial',
  progressState = '15 days left',
  progressValue = 50,
  className,
  style,
  onPrimaryClick,
  onSecondaryClick,
  onClose,
}: InfoCardProps) {
  const isLg = type === 'lg';
  const surfaceColor = isLg ? palette.orange['1'] : palette.green['1'];

  return (
    <div
      className={className}
      style={{
        width: SIDEBAR_WIDTH,
        boxSizing: 'border-box',
        // Inset on all 4 sides — the colored card (orange-1 / green-1)
        // is intended to breathe inside its sidebar slot; sibling rows
        // (Cells, User) are deliberately edge-to-edge by contrast.
        padding: spacing.scale['16'],
        display: 'flex',
        flexDirection: 'column',
        ...style,
      }}
    >
      <div
        style={{
          position: 'relative',
          width: '100%',
          boxSizing: 'border-box',
          padding: isLg ? spacing.scale['16'] : undefined,
          paddingTop: isLg ? undefined : spacing.scale['12'],
          paddingBottom: isLg ? undefined : spacing.scale['16'],
          paddingInline: isLg ? undefined : spacing.scale['16'],
          backgroundColor: surfaceColor,
          borderRadius: radius.scale.lg,
          display: 'flex',
          flexDirection: 'column',
          gap: isLg ? spacing.scale['12'] : spacing.scale['16'],
        }}
      >
        {isLg ? (
          <>
            <div
              style={{
                display: 'flex',
                flexDirection: 'column',
                gap: spacing.scale['6'],
                paddingBottom: spacing.scale['4'],
              }}
            >
              <div
                style={{
                  display: 'flex',
                  alignItems: 'flex-start',
                  gap: spacing.scale['6'],
                  paddingRight: spacing.scale['36'],
                }}
              >
                <span
                  aria-hidden="true"
                  style={{
                    display: 'inline-flex',
                    paddingBlock: spacing.scale['2'],
                    flexShrink: 0,
                    color: palette.orange['8'],
                  }}
                >
                  {leadIcon ?? (
                    <IconAlertLine
                      aria-hidden
                      style={{ width: spacing.scale['16'], height: spacing.scale['16'], display: 'block' }}
                    />
                  )}
                </span>
                <p style={{ ...labelTextStyle, flex: '1 0 0', minWidth: 0 }}>{label}</p>
              </div>
              <p style={captionTextStyle}>{caption}</p>
            </div>

            <Button
              variant="secondary"
              size="sm"
              shape="rounded"
              fullWidth
              onClick={onPrimaryClick}
              style={{ borderRadius: radius.scale.lg }}
            >
              {primaryButtonLabel}
            </Button>

            <Button
              variant="ghost"
              size="sm"
              shape="rounded"
              fullWidth
              onClick={onSecondaryClick}
              style={{ borderRadius: radius.scale.lg }}
            >
              {secondaryButtonLabel}
            </Button>

            {showCloseButton ? (
              <button
                type="button"
                onClick={onClose}
                aria-label="Close"
                style={{
                  position: 'absolute',
                  top: spacing.scale['12'],
                  right: spacing.scale['12'],
                  width: spacing.scale['16'],
                  height: spacing.scale['16'],
                  display: 'inline-flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  background: 'none',
                  border: 'none',
                  padding: 0,
                  cursor: 'pointer',
                  color: palette.gray['9a'],
                }}
              >
                <IconCloseLine
                  aria-hidden
                  style={{ width: spacing.scale['16'], height: spacing.scale['16'], display: 'block' }}
                />
              </button>
            ) : null}
          </>
        ) : (
          <>
            <div
              style={{
                display: 'flex',
                flexDirection: 'column',
                gap: spacing.scale['8'],
                width: '100%',
              }}
            >
              <ProgressBar
                direction="vertical"
                target="default"
                size="md"
                color="green"
                progressValue={progressValue}
                label={progressLabel}
                showLabel
                showOptionalLabel={false}
                showProgressState
                showTailIcon={false}
                showHelper={false}
                valueText={progressState}
                width={PROGRESS_BAR_WIDTH}
                style={{ width: '100%' }}
              />
            </div>

            <div
              style={{
                display: 'flex',
                flexDirection: 'column',
                gap: spacing.scale['8'],
                width: '100%',
              }}
            >
              <Button
                variant="secondary"
                size="sm"
                shape="rounded"
                fullWidth
                onClick={onPrimaryClick}
                style={{ borderRadius: radius.scale.lg }}
              >
                {primaryButtonLabel === 'Button' ? 'Upgrade' : primaryButtonLabel}
              </Button>
              <Button
                variant="secondary"
                size="sm"
                shape="rounded"
                fullWidth
                onClick={onSecondaryClick}
                style={{ borderRadius: radius.scale.lg }}
              >
                {secondaryButtonLabel === 'Button' ? 'Upgrade' : secondaryButtonLabel}
              </Button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

export default InfoCard;

API Reference

Props 문서 준비 중입니다. (component-spec.json 추가 시 표시됩니다.)