Skip to main content
This document provides standardized component specifications for the Paper UI design system. Use these as the baseline implementation for all MetaCogna Suite applications.

Overview

These baseline components follow Paper UI principles:
  • Digital Paper: Physical sheets with distinct borders and hard shadows
  • Hard Edges: Minimal border radius (max 2px)
  • High Contrast: Strict Ink/Paper polarity

PaperCard

The fundamental container component for content organization.

TypeScript Interface

interface PaperCardProps extends HTMLMotionProps<'div'> {
  title?: string;
  headerAction?: React.ReactNode;
  children: React.ReactNode;
  noPadding?: boolean;
  className?: string;
}

Implementation

export const PaperCard: React.FC<PaperCardProps> = ({ 
  title, 
  headerAction, 
  children, 
  className = '', 
  noPadding = false,
  ...props 
}) => {
  return (
    <motion.div 
      className={`bg-paper border-2 border-ink shadow-hard flex flex-col ${className}`}
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4, ease: "easeOut" }}
      {...props}
    >
      {title && (
        <div className="border-b-2 border-ink bg-surface px-6 py-3 flex justify-between items-center">
          <h3 className="font-serif font-bold text-lg text-ink tracking-wide uppercase">
            {title}
          </h3>
          {headerAction && <div>{headerAction}</div>}
        </div>
      )}
      <div className={noPadding ? '' : 'p-6'}>
        {children}
      </div>
    </motion.div>
  );
};

Usage

<PaperCard title="Card Title">
  Card content goes here
</PaperCard>

<PaperCard title="Card with Action" headerAction={<button>Action</button>}>
  Content with header action
</PaperCard>

<PaperCard noPadding>
  Content without padding
</PaperCard>

Styling Specifications

  • Background: bg-paper (white/light background)
  • Border: border-2 border-ink (2px solid border)
  • Shadow: shadow-hard (4px 4px 0px 0px)
  • Title Typography: Serif font, bold, uppercase, tracking-wide
  • Padding: p-6 (default) or none if noPadding prop

Dark Mode

Automatically handled via CSS custom properties:
  • Paper becomes dark in dark mode
  • Ink becomes light in dark mode
  • Shadows adapt automatically

PaperButton

Interactive button component with variants and sizes.

TypeScript Interface

interface PaperButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  fullWidth?: boolean;
}

Implementation

export const PaperButton: React.FC<PaperButtonProps> = ({ 
  children, 
  variant = 'primary', 
  size = 'md', 
  fullWidth = false,
  className = '',
  ...props 
}) => {
  const baseStyles = "font-bold font-sans border-2 border-ink transition-all duration-100 active:translate-x-[2px] active:translate-y-[2px] active:shadow-none flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed";
  
  const variants = {
    primary: "bg-ink text-paper hover:bg-accent hover:text-ink hover:border-ink shadow-hard",
    secondary: "bg-paper text-ink hover:bg-surface shadow-hard",
    ghost: "bg-transparent border-transparent hover:border-ink hover:bg-surface text-ink shadow-none hover:shadow-hard",
    danger: "bg-paper text-warning border-2 border-warning shadow-hard hover:bg-warning hover:text-paper hover:border-ink",
  };

  const sizes = {
    sm: "text-xs px-3 py-1.5",
    md: "text-sm px-5 py-2.5",
    lg: "text-base px-8 py-4",
  };

  const widthClass = fullWidth ? 'w-full' : '';

  return (
    <button 
      className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${widthClass} ${className}`}
      {...props}
    >
      {children}
    </button>
  );
};

Usage

<PaperButton variant="primary">Primary Button</PaperButton>
<PaperButton variant="secondary">Secondary Button</PaperButton>
<PaperButton variant="ghost">Ghost Button</PaperButton>
<PaperButton variant="danger">Danger Button</PaperButton>
<PaperButton size="sm">Small Button</PaperButton>
<PaperButton fullWidth>Full Width Button</PaperButton>

Variants

  • primary: Ink background, paper text, hover to accent
  • secondary: Paper background, ink text
  • ghost: Transparent, gains border and shadow on hover
  • danger: Warning color border and text

Active State

Buttons translate 2px in both X and Y directions on click (active:translate-x-[2px] active:translate-y-[2px]) to simulate physical button press.

PaperInput

Text input field with label and error support.

TypeScript Interface

interface PaperInputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
}

Implementation

export const PaperInput: React.FC<PaperInputProps> = ({ 
  label, 
  error, 
  className = '', 
  ...props 
}) => {
  return (
    <div className={`flex flex-col gap-1.5 ${className}`}>
      {label && (
        <label className="font-mono text-xs uppercase font-bold text-ink">
          {label}
        </label>
      )}
      <input 
        className="bg-paper border-2 border-ink p-3 text-ink font-mono text-sm placeholder-gray-400 focus:outline-none focus:ring-0 focus:shadow-hard transition-all duration-200"
        {...props}
      />
      {error && (
        <span className="text-red-600 dark:text-red-400 text-xs font-mono">
          {error}
        </span>
      )}
    </div>
  );
};

Usage

<PaperInput 
  label="USERNAME"
  placeholder="Enter username"
  value={username}
  onChange={(e) => setUsername(e.target.value)}
/>

<PaperInput 
  label="EMAIL"
  type="email"
  error="Invalid email address"
/>

Styling Specifications

  • Font: Monospace (font-mono) - Paper UI principle for technical inputs
  • Border: 2px solid ink border
  • Focus: Hard shadow (shadow-hard) instead of ring
  • Label: Monospace, uppercase, bold, tracking-wide
  • Error: Monospace font, warning color

PaperTextArea

Multi-line text input component.

TypeScript Interface

interface PaperTextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
  label?: string;
  error?: string;
}

Implementation

export const PaperTextArea: React.FC<PaperTextAreaProps> = ({ 
  label, 
  error, 
  className = '', 
  ...props 
}) => {
  return (
    <div className={`flex flex-col gap-1.5 ${className}`}>
      {label && (
        <label className="font-mono text-xs uppercase font-bold text-ink">
          {label}
        </label>
      )}
      <textarea 
        className="bg-paper border-2 border-ink p-3 text-ink font-mono text-sm placeholder-gray-400 focus:outline-none focus:ring-0 focus:shadow-hard transition-all duration-200 min-h-[120px]"
        {...props}
      />
      {error && (
        <span className="text-red-600 dark:text-red-400 text-xs font-mono">
          {error}
        </span>
      )}
    </div>
  );
};

Usage

<PaperTextArea 
  label="DESCRIPTION"
  rows={5}
  value={description}
  onChange={(e) => setDescription(e.target.value)}
/>

PaperSelect

Dropdown select component with custom styling.

TypeScript Interface

interface PaperSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
  label?: string;
  error?: string;
  options: { value: string; label: string }[];
}

Implementation

export const PaperSelect: React.FC<PaperSelectProps> = ({ 
  label, 
  error, 
  options, 
  className = '', 
  ...props 
}) => {
  return (
    <div className={`flex flex-col gap-1.5 ${className}`}>
      {label && (
        <label className="font-mono text-xs uppercase font-bold text-ink">
          {label}
        </label>
      )}
      <div className="relative">
        <select 
          className="appearance-none w-full bg-paper border-2 border-ink p-3 text-ink font-sans focus:outline-none focus:ring-0 focus:shadow-hard transition-all duration-200"
          {...props}
        >
          {options.map(opt => (
            <option key={opt.value} value={opt.value}>
              {opt.label}
            </option>
          ))}
        </select>
        <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-ink">
          <svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
            <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
          </svg>
        </div>
      </div>
      {error && (
        <span className="text-red-600 dark:text-red-400 text-xs font-mono">
          {error}
        </span>
      )}
    </div>
  );
};

Usage

<PaperSelect
  label="LANGUAGE"
  options={[
    { value: 'en', label: 'English' },
    { value: 'es', label: 'Spanish' },
  ]}
  value={language}
  onChange={(e) => setLanguage(e.target.value)}
/>

PaperBadge

Status indicator and label component.

TypeScript Interface

interface PaperBadgeProps {
  children: React.ReactNode;
  color?: 'emerald' | 'blue' | 'purple' | 'orange' | 'red' | 'gray';
}

Implementation

export const PaperBadge: React.FC<PaperBadgeProps> = ({ 
  children, 
  color = 'gray' 
}) => {
  const colors = {
    emerald: "bg-emerald-50 dark:bg-emerald-900/50 text-emerald-800 dark:text-emerald-200 border-emerald-200 dark:border-emerald-700",
    blue: "bg-blue-50 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-700",
    purple: "bg-purple-50 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200 border-purple-200 dark:border-purple-700",
    orange: "bg-orange-50 dark:bg-orange-900/50 text-orange-800 dark:text-orange-200 border-orange-200 dark:border-orange-700",
    red: "bg-red-50 dark:bg-red-900/50 text-red-800 dark:text-red-200 border-red-200 dark:border-red-700",
    gray: "bg-gray-50 dark:bg-gray-800 text-gray-800 dark:text-gray-200 border-gray-200 dark:border-gray-600",
  };

  return (
    <span className={`inline-flex items-center px-2.5 py-0.5 border text-xs font-mono font-bold uppercase tracking-wider ${colors[color]}`}>
      {children}
    </span>
  );
};

Usage

<PaperBadge color="emerald">SUCCESS</PaperBadge>
<PaperBadge color="red">ERROR</PaperBadge>
<PaperBadge color="blue">INFO</PaperBadge>

Styling Specifications

  • Font: Monospace, uppercase, bold, tracking-wider
  • Size: text-xs (12px) for readability
  • Colors: Supports semantic colors with dark mode variants

PaperModal

Modal dialog component with overlay and animations.

TypeScript Interface

interface PaperModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
  maxWidth?: string;
}

Implementation

export const PaperModal: React.FC<PaperModalProps> = ({ 
  isOpen, 
  onClose, 
  title, 
  children, 
  maxWidth = "max-w-lg" 
}) => {
  return (
    <AnimatePresence>
      {isOpen && (
        <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
          <motion.div 
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="absolute inset-0 bg-ink/30 backdrop-blur-sm"
            onClick={onClose}
          />
          <motion.div
            initial={{ opacity: 0, scale: 0.95, y: 10 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.95, y: 10 }}
            className={`relative z-10 w-full ${maxWidth} max-h-[95vh] overflow-y-auto`}
          >
            <PaperCard 
              title={title} 
              headerAction={
                <button onClick={onClose} className="hover:text-accent transition-colors text-ink">
                  <X className="w-5 h-5" />
                </button>
              }
            >
              {children}
            </PaperCard>
          </motion.div>
        </div>
      )}
    </AnimatePresence>
  );
};

Usage

<PaperModal 
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  title="Modal Title"
>
  Modal content goes here
</PaperModal>

Animation Guidelines

Card Animations

Cards should use subtle fade-in animations:
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}

Button Interactions

Buttons should translate on active state:
active:translate-x-[2px] active:translate-y-[2px] active:shadow-none
Modals should scale and fade:
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}

Dark Mode Support

All components automatically support dark mode through CSS custom properties:
  • Colors: Defined in :root and .dark selectors
  • Components: Use semantic tokens (bg-paper, text-ink, etc.)
  • No changes needed: Components adapt automatically
Ensure your Tailwind configuration uses CSS custom properties for colors. See Tailwind Configuration for details.

Dependencies

Required dependencies:
{
  "dependencies": {
    "react": "^18.0.0",
    "framer-motion": "^10.0.0"
  },
  "devDependencies": {
    "tailwindcss": "^3.0.0",
    "@types/react": "^18.0.0"
  }
}