> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Card Form Layout Builder

> Design custom card payment form layouts with live preview and platform-specific code generation.

export const ColorPicker = ({value, onChange, label}) => <div>
    {label && <div style={{
  fontSize: DS.font.xs,
  color: DS.color.textSecondary,
  marginBottom: DS.space.xs
}}>{label}</div>}
    <div style={{
  display: 'flex',
  gap: DS.space.sm,
  alignItems: 'center'
}}>
      <div style={{
  position: 'relative'
}}>
        <div style={{
  width: 32,
  height: 32,
  borderRadius: DS.radius.sm,
  background: value,
  border: `1px solid ${DS.color.border}`,
  cursor: 'pointer'
}} />
        <input type="color" value={value.startsWith('#') ? value : '#2f98ff'} onChange={e => onChange(e.target.value)} aria-label={label} style={{
  position: 'absolute',
  inset: 0,
  opacity: 0,
  cursor: 'pointer',
  width: '100%',
  height: '100%'
}} />
      </div>
      <input type="text" value={value} onChange={e => onChange(e.target.value)} style={{
  flex: 1,
  padding: `${DS.space.xs}px ${DS.space.sm}px`,
  border: `1px solid ${DS.color.border}`,
  borderRadius: DS.radius.sm,
  fontFamily: DS.font.mono,
  fontSize: DS.font.xs,
  color: DS.color.text,
  background: DS.color.bgSurface,
  outline: 'none',
  transition: `border-color ${DS.transition.fast}`
}} onFocus={e => e.target.style.borderColor = DS.color.brand} onBlur={e => e.target.style.borderColor = DS.color.border} />
    </div>
  </div>;

export const Slider = ({min, max, value, onChange, label, unit = 'px', step}) => {
  const pct = (value - min) / (max - min) * 100;
  return <div>
      {label && <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: DS.space.xs
  }}>
          <span style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>{label}</span>
          <span style={{
    fontSize: DS.font.xs,
    fontFamily: DS.font.mono,
    fontWeight: 600,
    color: DS.color.text,
    background: DS.color.bgSubtle,
    padding: `1px ${DS.space.sm}px`,
    borderRadius: DS.radius.sm,
    border: `1px solid ${DS.color.border}`
  }}>{value}{unit}</span>
        </div>}
      <div style={{
    position: 'relative',
    height: 20,
    display: 'flex',
    alignItems: 'center'
  }}>
        <div style={{
    position: 'absolute',
    left: 0,
    right: 0,
    height: 4,
    borderRadius: 2,
    background: DS.color.border
  }} />
        <div style={{
    position: 'absolute',
    left: 0,
    width: `${pct}%`,
    height: 4,
    borderRadius: 2,
    background: DS.color.brand,
    transition: `width ${DS.transition.fast}`
  }} />
        <input type="range" min={min} max={max} step={step || 1} value={value} onChange={e => onChange(parseInt(e.target.value))} aria-label={label} style={{
    position: 'absolute',
    width: '100%',
    height: 20,
    opacity: 0,
    cursor: 'pointer',
    margin: 0
  }} />
        <div style={{
    position: 'absolute',
    left: `calc(${pct}% - 8px)`,
    width: 16,
    height: 16,
    borderRadius: '50%',
    background: DS.color.bgSurface,
    border: `2px solid ${DS.color.brand}`,
    boxShadow: '0 1px 4px rgba(0,0,0,0.12)',
    pointerEvents: 'none',
    transition: `left ${DS.transition.fast}`
  }} />
      </div>
      <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    marginTop: 2,
    fontSize: DS.font.xxs,
    color: DS.color.textMuted
  }}>
        <span>{min}{unit}</span>
        <span>{max}{unit}</span>
      </div>
    </div>;
};

export const CopyButton = ({getText}) => {
  const [copied, setCopied] = useState(false);
  const [hovered, setHovered] = useState(false);
  const handleCopy = () => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(getText());
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    }
  };
  return <button onClick={handleCopy} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{
    display: 'flex',
    alignItems: 'center',
    gap: DS.space.xs,
    padding: `${DS.space.xs}px ${DS.space.sm + 2}px`,
    border: `1px solid ${copied ? DS.color.success : hovered ? DS.color.borderHover : DS.color.border}`,
    borderRadius: DS.radius.sm,
    background: copied ? DS.color.successLight : hovered ? DS.color.bgSubtle : DS.color.bgSurface,
    fontSize: DS.font.xxs,
    fontWeight: 600,
    color: copied ? DS.color.success : DS.color.textSecondary,
    cursor: 'pointer',
    transition: `all ${DS.transition.fast}`,
    outline: 'none'
  }}>{copied ? '✓ Copied' : '⎘ Copy'}</button>;
};

export const TabBar = ({tabs, activeTab, onTabChange, extra}) => {
  const TabBtn = ({active, onClick, label}) => {
    const [hovered, setHovered] = useState(false);
    return <button onClick={onClick} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{
      padding: `${DS.space.sm + 2}px ${DS.space.lg}px`,
      fontSize: DS.font.xxs,
      fontWeight: 700,
      textTransform: 'uppercase',
      letterSpacing: '0.8px',
      cursor: 'pointer',
      border: 'none',
      background: 'transparent',
      color: active ? DS.color.brand : hovered ? DS.color.textSecondary : DS.color.textMuted,
      borderBottom: active ? `2px solid ${DS.color.brand}` : '2px solid transparent',
      position: 'relative',
      top: 1,
      transition: `color ${DS.transition.fast}`,
      outline: 'none'
    }}>{label}</button>;
  };
  return <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: 0,
    borderBottom: `1px solid ${DS.color.border}`,
    background: DS.color.bgSubtle
  }}>
      {tabs.map(tab => <TabBtn key={tab.id} active={activeTab === tab.id} onClick={() => onTabChange(tab.id)} label={tab.label} />)}
      {extra && <div style={{
    marginLeft: 'auto',
    paddingRight: DS.space.md
  }}>{extra}</div>}
    </div>;
};

export const HoverButton = ({children, active, onClick, style: customStyle, activeStyle, hoverStyle: customHover}) => {
  const [hovered, setHovered] = useState(false);
  const [pressed, setPressed] = useState(false);
  const baseStyle = Object.assign({
    cursor: 'pointer',
    border: `2px solid ${active ? DS.color.brand : DS.color.border}`,
    borderRadius: DS.radius.md,
    background: active ? DS.color.brandLight : DS.color.bgSurface,
    transition: `all ${DS.transition.fast}`,
    outline: 'none'
  }, customStyle);
  const hoverMerge = !active && hovered ? Object.assign({
    borderColor: DS.color.borderHover,
    background: DS.color.bgSubtle
  }, customHover) : {};
  const activeMerge = active ? Object.assign({
    borderColor: DS.color.brand,
    background: DS.color.brandLight
  }, activeStyle) : {};
  const pressedMerge = pressed ? {
    transform: 'scale(0.98)'
  } : {};
  const mergedStyle = Object.assign({}, baseStyle, hoverMerge, activeMerge, pressedMerge);
  return <button onClick={onClick} onMouseEnter={() => setHovered(true)} onMouseLeave={() => {
    setHovered(false);
    setPressed(false);
  }} onMouseDown={() => setPressed(true)} onMouseUp={() => setPressed(false)} style={mergedStyle}>{children}</button>;
};

export const Badge = ({children, color = DS.color.brand}) => <span style={{
  fontSize: DS.font.xxs - 1,
  fontWeight: 700,
  padding: `1px ${DS.space.sm - 2}px`,
  borderRadius: DS.radius.sm,
  background: `${color}15`,
  color: color,
  textTransform: 'uppercase',
  letterSpacing: '0.3px',
  whiteSpace: 'nowrap'
}}>{children}</span>;

export const Toggle = ({on, onClick, disabled, label}) => {
  const [hovered, setHovered] = useState(false);
  return <button role="switch" aria-checked={on} aria-label={label} onClick={onClick} disabled={disabled} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{
    width: 36,
    height: 20,
    borderRadius: 10,
    background: on ? DS.color.brand : hovered && !disabled ? DS.color.borderHover : DS.color.border,
    position: 'relative',
    cursor: disabled ? 'not-allowed' : 'pointer',
    border: 'none',
    opacity: disabled ? 0.4 : 1,
    flexShrink: 0,
    transition: `background ${DS.transition.fast}`,
    outline: 'none'
  }}>
      <div style={{
    position: 'absolute',
    width: 16,
    height: 16,
    borderRadius: '50%',
    background: '#fff',
    top: 2,
    left: on ? 18 : 2,
    transition: `left ${DS.transition.normal}`,
    boxShadow: '0 1px 3px rgba(0,0,0,0.18)'
  }} />
    </button>;
};

export const Panel = ({title, children, style}) => <div style={Object.assign({
  background: DS.color.bgSurface,
  border: `1px solid ${DS.color.border}`,
  borderRadius: DS.radius.lg,
  overflow: 'hidden'
}, style)}>
    {title && <div style={{
  padding: `${DS.space.sm}px ${DS.space.lg}px`,
  borderBottom: `1px solid ${DS.color.border}`,
  fontSize: DS.font.xxs,
  fontWeight: 700,
  textTransform: 'uppercase',
  letterSpacing: '1px',
  color: DS.color.textMuted,
  background: DS.color.bgSubtle
}}>
        {title}
      </div>}
    {children}
  </div>;

export const DS = {
  color: {
    brand: 'var(--ds-color-brand, #2f98ff)',
    brandLight: 'var(--ds-color-brand-light, #e8f2ff)',
    brandMuted: 'var(--ds-color-brand-muted, rgba(47, 152, 255, 0.12))',
    focus: 'var(--ds-color-brand, #2f98ff)',
    success: 'var(--ds-color-success, #1a8a5c)',
    successLight: 'var(--ds-color-success-light, #edf8f2)',
    successMuted: 'var(--ds-color-success-muted, rgba(26, 138, 92, 0.12))',
    warning: 'var(--ds-color-warning, #c47a20)',
    warningLight: 'var(--ds-color-warning-light, #fef6eb)',
    warningMuted: 'var(--ds-color-warning-muted, rgba(196, 122, 32, 0.12))',
    error: 'var(--ds-color-error, #c44040)',
    errorLight: 'var(--ds-color-error-light, #fef0f0)',
    errorMuted: 'rgba(196, 64, 64, 0.12)',
    purple: '#7c5cbf',
    purpleLight: '#f3effc',
    purpleMuted: 'rgba(124, 92, 191, 0.12)',
    text: 'var(--ds-color-text, #1c1b18)',
    textSecondary: 'var(--ds-color-text-secondary, #5c5953)',
    textMuted: 'var(--ds-color-text-muted, #9d9a92)',
    textDisabled: '#c5c2ba',
    border: 'var(--ds-color-border, #e4e2dd)',
    borderHover: 'var(--ds-color-border-hover, #d0cec8)',
    bgPage: 'var(--ds-color-bg-page, #f5f4f1)',
    bgSurface: 'var(--ds-color-bg-surface, #ffffff)',
    bgSubtle: 'var(--ds-color-bg-subtle, #fafaf8)',
    bgOverlay: 'rgba(0, 0, 0, 0.04)'
  },
  space: {
    xxs: 2,
    xs: 4,
    sm: 8,
    md: 12,
    lg: 16,
    xl: 20,
    xxl: 24,
    xxxl: 32
  },
  font: {
    xxs: 10,
    xs: 11,
    sm: 12,
    md: 13,
    base: 14,
    lg: 15,
    xl: 16,
    mono: "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace",
    sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
  },
  radius: {
    sm: 4,
    md: 8,
    lg: 12
  },
  transition: {
    fast: '120ms ease',
    normal: '200ms ease'
  }
};

export const FORM_FIELDS = [{
  id: 'cardNumber',
  label: 'Card Number',
  placeholder: '4242 4242 4242 4242',
  required: true,
  color: DS.color.warning,
  webTag: 'primer-input-card-number',
  iosMethod: 'PrimerCardNumberField',
  androidComp: 'CardFormDefaults.CardNumberField'
}, {
  id: 'expiry',
  label: 'Expiry',
  placeholder: 'MM/YY',
  required: true,
  color: DS.color.warning,
  webTag: 'primer-input-card-expiry',
  iosMethod: 'PrimerExpiryDateField',
  androidComp: 'CardFormDefaults.ExpiryField'
}, {
  id: 'cvv',
  label: 'CVV',
  placeholder: '123',
  required: true,
  color: DS.color.warning,
  webTag: 'primer-input-cvv',
  iosMethod: 'PrimerCvvField',
  androidComp: 'CardFormDefaults.CvvField'
}, {
  id: 'cardholder',
  label: 'Cardholder Name',
  placeholder: 'J. Smith',
  required: false,
  color: DS.color.textMuted,
  webTag: 'primer-input-card-holder-name',
  iosMethod: 'PrimerCardholderNameField',
  androidComp: 'CardFormDefaults.CardholderField'
}, {
  id: 'submit',
  label: 'Submit Button',
  placeholder: 'Pay Now',
  required: false,
  color: DS.color.brand,
  webTag: 'primer-card-form-submit',
  iosMethod: null,
  androidComp: 'CardFormDefaults.SubmitButton'
}];

export const LAYOUTS = [{
  id: 'vertical',
  name: 'Vertical',
  desc: 'All fields stacked',
  rows: [['cardNumber'], ['expiry'], ['cvv'], ['cardholder'], ['submit']]
}, {
  id: 'grouped',
  name: 'Grouped',
  desc: 'Expiry + CVV side by side',
  rows: [['cardNumber'], ['expiry', 'cvv'], ['cardholder'], ['submit']]
}, {
  id: 'compact',
  name: 'Compact',
  desc: 'Minimal height, 2-col',
  rows: [['cardNumber'], ['expiry', 'cvv'], ['cardholder', 'submit']]
}, {
  id: 'single-line',
  name: 'Single-line',
  desc: 'Card fields in one row',
  rows: [['cardNumber', 'expiry', 'cvv'], ['cardholder'], ['submit']]
}];

export const LayoutIcon = ({layoutId, active}) => {
  const patterns = {
    'vertical': [[1], [1], [1], [1]],
    'grouped': [[1], [0.48, 0.48], [1], [1]],
    'compact': [[1], [0.48, 0.48], [0.48, 0.48]],
    'single-line': [[0.3, 0.3, 0.3], [1], [1]]
  };
  const rows = patterns[layoutId] || [[1]];
  return <div style={{
    width: 36,
    height: 28,
    borderRadius: DS.radius.sm,
    background: active ? DS.color.bgSurface : DS.color.bgSubtle,
    border: '1px solid ' + (active ? DS.color.brand : DS.color.border),
    display: 'flex',
    flexDirection: 'column',
    gap: 2,
    padding: '3px 4px',
    flexShrink: 0
  }}>
      {rows.map((row, ri) => <div key={ri} style={{
    display: 'flex',
    gap: 2
  }}>
          {row.map((w, ci) => <div key={ci} style={{
    height: 3,
    borderRadius: 1,
    flex: w,
    background: active ? DS.color.brand : DS.color.borderHover
  }} />)}
        </div>)}
    </div>;
};

export const PLATFORM_ICONS = {
  web: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="12" r="10" />
      <path d="M2 12h20" />
      <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
    </svg>,
  ios: <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
      <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
    </svg>,
  android: <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
      <path d="M17.6 11.48V7.45c0-.29-.24-.53-.53-.53H6.93c-.29 0-.53.24-.53.53v4.03l.01.09c0 .29.24.53.53.53h10.14c.29 0 .53-.24.53-.53v-.09zM6.4 7.45V4.85h11.2v2.6H6.4zm1.9-4.2l-.76-1.35c-.08-.15-.03-.33.11-.41.15-.08.33-.03.41.11l.78 1.39c.64-.27 1.36-.42 2.12-.42s1.48.15 2.12.42l.78-1.39c.08-.15.26-.19.41-.11.14.08.19.26.11.41l-.76 1.35c1.43.72 2.42 2.06 2.62 3.63H5.67c.21-1.57 1.2-2.91 2.63-3.63zM10 5.37c0-.28-.22-.5-.5-.5s-.5.22-.5.5.22.5.5.5.5-.22.5-.5zm5 0c0-.28-.22-.5-.5-.5s-.5.22-.5.5.22.5.5.5.5-.22.5-.5zM5.4 13.22c-.55 0-1-.45-1-1V8.48c0-.55.45-1 1-1s1 .45 1 1v3.74c0 .55-.45 1-1 1zM18.6 13.22c-.55 0-1-.45-1-1V8.48c0-.55.45-1 1-1s1 .45 1 1v3.74c0 .55-.45 1-1 1zM7.4 18.66V13.4h9.2v5.26c0 .49-.4.88-.88.88h-.96v2.33c0 .55-.45 1-1 1s-1-.45-1-1v-2.33h-1.52v2.33c0 .55-.45 1-1 1s-1-.45-1-1v-2.33h-.96c-.49 0-.88-.39-.88-.88z" />
    </svg>
};

export const SimInputWeb = ({label, placeholder, hideLabels, hasIcon, styles}) => <div style={{
  border: (styles ? styles.primerWidthDefault + 'px' : '1.5px') + ' solid ' + (styles ? styles.primerColorBorderOutlinedDefault : DS.color.border),
  borderRadius: styles ? styles.primerRadiusSmall : DS.radius.md,
  background: styles ? styles.primerColorBackgroundPrimary : DS.color.bgSurface,
  overflow: 'hidden'
}}>
    {!hideLabels && <span style={{
  fontSize: DS.font.xs,
  fontWeight: 500,
  color: styles ? styles.primerColorTextPrimary : DS.color.textSecondary,
  padding: DS.space.sm + 'px ' + DS.space.md + 'px 0',
  display: 'block'
}}>{label}</span>}
    <div style={{
  padding: DS.space.xs + 2 + 'px ' + DS.space.md + 'px ' + (DS.space.sm + 2) + 'px',
  fontSize: DS.font.lg,
  color: styles ? styles.primerColorTextPlaceholder : DS.color.textDisabled,
  display: 'flex',
  alignItems: 'center',
  gap: DS.space.sm
}}>
      {hasIcon && <span style={{
  fontSize: DS.font.xl
}}></span>}
      {placeholder}
    </div>
  </div>;

export const SimInputIos = ({label, placeholder, hasIcon, styling}) => <div style={{
  border: (styling.borderWidth || 1) + 'px solid ' + (styling.borderColor || '#E4E2DD'),
  borderRadius: styling.cornerRadius || 10,
  background: styling.backgroundColor || '#FFFFFF',
  overflow: 'hidden',
  height: styling.fieldHeight || 48,
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'center',
  fontFamily: '-apple-system, system-ui, sans-serif',
  padding: '0 ' + (styling.paddingH || 14) + 'px'
}}>
    <span style={{
  fontSize: styling.labelFontSize || 11,
  fontWeight: 500,
  color: styling.labelColor || '#8E8E93',
  paddingTop: styling.paddingV || 6,
  display: 'block'
}}>{label}</span>
    <div style={{
  paddingBottom: styling.paddingV || 6,
  fontSize: styling.fontSize || 16,
  fontWeight: styling.fontWeight || 400,
  color: styling.textColor || styling.placeholderColor || '#C7C7CC',
  display: 'flex',
  alignItems: 'center',
  gap: 6
}}>
      {hasIcon && <span style={{
  fontSize: 14
}}></span>}
      {placeholder}
    </div>
  </div>;

export const SimInputAndroid = ({label, placeholder, hasIcon, theme}) => <div style={{
  border: (theme.borderWidth || 1) + 'px solid ' + (theme.borderColor || '#E0E0E0'),
  borderRadius: theme.inputMode === 'UNDERLINED' ? 0 : theme.cornerRadius || 8,
  borderTop: theme.inputMode === 'UNDERLINED' ? 'none' : undefined,
  borderLeft: theme.inputMode === 'UNDERLINED' ? 'none' : undefined,
  borderRight: theme.inputMode === 'UNDERLINED' ? 'none' : undefined,
  background: theme.backgroundColor || '#FFFFFF',
  overflow: 'hidden',
  height: 56,
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'center',
  fontFamily: 'Roboto, sans-serif'
}}>
    <span style={{
  fontSize: 12,
  fontWeight: 400,
  color: theme.labelColor || '#757575',
  padding: '8px 16px 0',
  display: 'block',
  overflow: 'hidden',
  whiteSpace: 'nowrap',
  textOverflow: 'ellipsis'
}}>{label}</span>
    <div style={{
  padding: '0 16px 8px',
  fontSize: 16,
  color: theme.inputTextColor || theme.placeholderColor || '#9E9E9E',
  display: 'flex',
  alignItems: 'center',
  gap: 8,
  overflow: 'hidden',
  whiteSpace: 'nowrap',
  textOverflow: 'ellipsis'
}}>
      {hasIcon && <span style={{
  fontSize: 14,
  flexShrink: 0
}}></span>}
      <span style={{
  overflow: 'hidden',
  whiteSpace: 'nowrap',
  textOverflow: 'ellipsis'
}}>{placeholder}</span>
    </div>
  </div>;

export const TextInput = ({value, onChange, placeholder}) => <input type="text" value={value} onChange={e => onChange(e.target.value)} placeholder={placeholder} style={{
  width: '100%',
  padding: DS.space.xs + 1 + 'px ' + DS.space.sm + 'px',
  border: '1px solid ' + DS.color.border,
  borderRadius: DS.radius.sm,
  fontFamily: DS.font.mono,
  fontSize: DS.font.xs,
  color: DS.color.text,
  background: DS.color.bgSurface,
  outline: 'none'
}} onFocus={e => {
  e.target.style.borderColor = DS.color.brand;
}} onBlur={e => {
  e.target.style.borderColor = DS.color.border;
}} />;

export const DEFAULT_LABELS = {
  cardNumber: 'Card Number',
  expiry: 'Expiry',
  cvv: 'CVV',
  cardholder: 'Cardholder Name'
};
export const DEFAULT_PLACEHOLDERS = {
  cardNumber: '4242 4242 4242 4242',
  expiry: 'MM/YY',
  cvv: '123',
  cardholder: 'J. Smith'
};

export const ENABLED_DEFAULTS = {
  cardNumber: true,
  expiry: true,
  cvv: true,
  cardholder: true,
  submit: true
};

export const IOS_STYLING_DEFAULTS = {
  cornerRadius: 10,
  fontSize: 16,
  fieldHeight: 48,
  borderColor: '#E4E2DD',
  focusedBorderColor: '#2F98FF',
  backgroundColor: '#FFFFFF',
  labelFontSize: 11,
  labelColor: '#8E8E93',
  placeholderColor: '#C7C7CC',
  errorBorderColor: '#FF3B30',
  borderWidth: 1,
  paddingV: 6,
  paddingH: 14,
  textColor: '#1C1B18',
  fontWeight: 400,
  fontName: '',
  labelFontName: '',
  labelFontWeight: 400
};

export const IOS_CHECKOUT_THEME_DEFAULTS = {
  primerColorBrand: '#2F98FF',
  primerColorBackground: '#FFFFFF',
  primerColorTextPrimary: '#1C1B18',
  primerColorTextSecondary: '#5C5953',
  primerColorTextPlaceholder: '#9D9A92',
  primerColorTextDisabled: '#C5C2BA',
  primerColorBorderOutlinedDefault: '#E4E2DD',
  primerColorBorderOutlinedFocus: '#2F98FF',
  primerColorBorderOutlinedError: '#C44040',
  primerColorFocus: '#2F98FF',
  primerColorRed500: '#C44040',
  primerColorGreen500: '#1A8A5C',
  primerRadiusMedium: 8,
  primerRadiusSmall: 4,
  primerBorderWidthThin: 1,
  primerBorderWidthMedium: 2
};

export const ANDROID_THEME_DEFAULTS = {
  inputMode: 'OUTLINED',
  cornerRadius: 8,
  backgroundColor: '#FFFFFF',
  borderColor: '#E0E0E0',
  borderWidth: 1,
  labelColor: '#757575',
  placeholderColor: '#9E9E9E',
  buttonColor: '#2F98FF',
  buttonCornerRadius: 8,
  focusedBorderColor: '#2F98FF',
  errorBorderColor: '#C44040',
  inputTextColor: '#1C1B18',
  cursorColor: '#2F98FF',
  buttonTextColor: '#FFFFFF'
};

export const WEB_STYLE_DEFAULTS = {
  primerColorBrand: '#2F98FF',
  primerColorBackgroundPrimary: '#FFFFFF',
  primerColorTextPrimary: '#1C1B18',
  primerColorTextPlaceholder: '#9D9A92',
  primerColorBorderOutlinedDefault: '#E4E2DD',
  primerColorBorderOutlinedFocus: '#2F98FF',
  primerColorBorderOutlinedError: '#C44040',
  primerColorFocus: '#2F98FF',
  primerColorTextNegative: '#C44040',
  primerRadiusSmall: 4,
  primerRadiusMedium: 8,
  primerWidthDefault: 1,
  primerWidthFocus: 2,
  primerSpaceSmall: 8
};

export const toCssVar = key => '--' + key.replace(/([A-Z])/g, '-$1').toLowerCase();

export const getWebCode = (activeTab, layout, enabled, hideLabels, isDisabled, cardholderVisible, cardholderRequired, showAmount, buttonText, fieldLabels, fieldPlaceholders, cardholderDefault, showWebStyles, webStyles) => {
  const TAG_MAP = {
    cardNumber: 'primer-input-card-number',
    expiry: 'primer-input-card-expiry',
    cvv: 'primer-input-cvv',
    cardholder: 'primer-input-card-holder-name',
    submit: 'primer-card-form-submit'
  };
  var buildTagAttrs = function (id) {
    var parts = [];
    if (fieldLabels && fieldLabels[id] && fieldLabels[id] !== DEFAULT_LABELS[id]) {
      parts.push(' label="' + fieldLabels[id] + '"');
    }
    if (fieldPlaceholders && fieldPlaceholders[id] && fieldPlaceholders[id] !== DEFAULT_PLACEHOLDERS[id]) {
      parts.push(' placeholder="' + fieldPlaceholders[id] + '"');
    }
    return parts.join('');
  };
  const currentLayout = LAYOUTS.find(l => l.id === layout);
  if (activeTab === 'html') {
    var attrs = [];
    if (hideLabels) attrs.push(' hide-labels');
    if (isDisabled) attrs.push(' disabled');
    var lines = ['<primer-card-form' + attrs.join('') + '>'];
    lines.push('  <div slot="card-form-content" class="card-form-' + layout + '">');
    if (currentLayout) {
      currentLayout.rows.forEach(function (row) {
        var active = row.filter(function (id) {
          return enabled[id];
        });
        if (!active.length) return;
        if (active.length === 1) {
          var tag = TAG_MAP[active[0]];
          var extra = buildTagAttrs(active[0]);
          if (tag) lines.push('    <' + tag + extra + '></' + tag + '>');
        } else {
          lines.push('    <div class="row">');
          active.forEach(function (id) {
            var tag = TAG_MAP[id];
            var extra = buildTagAttrs(id);
            if (tag) lines.push('      <' + tag + extra + '></' + tag + '>');
          });
          lines.push('    </div>');
        }
      });
    }
    lines.push('  </div>');
    lines.push('</primer-card-form>');
    return lines.join('\n');
  }
  if (activeTab === 'css') {
    var layoutCss = '.card-form-' + layout + ' {\n  display: flex;\n  flex-direction: column;\n  gap: var(--primer-space-small);\n}\n\n.card-form-' + layout + ' .row {\n  display: flex;\n  gap: var(--primer-space-small);\n}\n\n.card-form-' + layout + ' .row > * {\n  flex: 1;\n}';
    if (showWebStyles && webStyles) {
      var tokenLines = [];
      Object.keys(WEB_STYLE_DEFAULTS).forEach(function (key) {
        if (webStyles[key] !== WEB_STYLE_DEFAULTS[key]) {
          var cssVal = typeof webStyles[key] === 'number' ? webStyles[key] + 'px' : webStyles[key];
          tokenLines.push('  ' + toCssVar(key) + ': ' + cssVal + ';');
        }
      });
      if (tokenLines.length) {
        layoutCss += '\n\nprimer-checkout {\n' + tokenLines.join('\n') + '\n}';
      }
    }
    return layoutCss;
  }
  var jsLines = [];
  jsLines.push("const form = document.querySelector('primer-card-form');");
  jsLines.push("const checkout = document.querySelector('primer-checkout');");
  jsLines.push('');
  var hasClientOptions = cardholderVisible || showAmount || buttonText !== 'Pay Now';
  if (hasClientOptions) {
    jsLines.push('const options = {');
    if (cardholderVisible) {
      jsLines.push('  clientOptions: {');
      jsLines.push('    card: {');
      jsLines.push('      cardholderName: {');
      jsLines.push('        visible: true,');
      if (cardholderRequired) jsLines.push('        required: true,');
      if (cardholderDefault) jsLines.push("        defaultValue: '" + cardholderDefault + "',");
      jsLines.push('      },');
      jsLines.push('    },');
      jsLines.push('  },');
    }
    if (showAmount || buttonText !== 'Pay Now') {
      jsLines.push('  submitButton: {');
      if (showAmount) jsLines.push('    amountVisible: true,');
      if (buttonText !== 'Pay Now') jsLines.push("    buttonText: '" + buttonText + "',");
      jsLines.push('  },');
    }
    jsLines.push('};');
    jsLines.push('');
  }
  jsLines.push("checkout.addEventListener('primer:state-change', (e) => {");
  jsLines.push('  if (e.detail.isSuccessful) {');
  jsLines.push("    window.location.href = '/confirmation';");
  jsLines.push('  }');
  jsLines.push('  if (e.detail.paymentFailure) {');
  jsLines.push('    console.error(e.detail.paymentFailure.message);');
  jsLines.push('  }');
  jsLines.push('});');
  return jsLines.join('\n');
};

export const toSwiftColor = hex => {
  var r = parseInt(hex.slice(1, 3), 16);
  var g = parseInt(hex.slice(3, 5), 16);
  var b = parseInt(hex.slice(5, 7), 16);
  return 'Color(red: ' + (r / 255).toFixed(2) + ', green: ' + (g / 255).toFixed(2) + ', blue: ' + (b / 255).toFixed(2) + ')';
};

export const getIosCode = (activeTab, layout, enabled, showStyling, styling, submitButtonText, fieldLabels, fieldPlaceholders, showCheckoutTheme, checkoutTheme) => {
  const currentLayout = LAYOUTS.find(l => l.id === layout);
  var stylingCode = '';
  if (showStyling) {
    var stylingLines = ['let fieldStyling = PrimerFieldStyling('];
    if (styling.fontName) stylingLines.push('  fontName: "' + styling.fontName + '",');
    stylingLines.push('  fontSize: ' + styling.fontSize + ',');
    if (styling.fontWeight !== 400) stylingLines.push('  fontWeight: ' + styling.fontWeight + ',');
    if (styling.labelFontName) stylingLines.push('  labelFontName: "' + styling.labelFontName + '",');
    stylingLines.push('  labelFontSize: ' + styling.labelFontSize + ',');
    if (styling.labelFontWeight !== 400) stylingLines.push('  labelFontWeight: ' + styling.labelFontWeight + ',');
    if (styling.textColor !== '#1C1B18') stylingLines.push('  textColor: ' + toSwiftColor(styling.textColor) + ',');
    stylingLines.push('  labelColor: ' + toSwiftColor(styling.labelColor) + ',');
    stylingLines.push('  backgroundColor: ' + toSwiftColor(styling.backgroundColor) + ',');
    stylingLines.push('  borderColor: ' + toSwiftColor(styling.borderColor) + ',');
    stylingLines.push('  focusedBorderColor: ' + toSwiftColor(styling.focusedBorderColor) + ',');
    stylingLines.push('  errorBorderColor: ' + toSwiftColor(styling.errorBorderColor) + ',');
    stylingLines.push('  placeholderColor: ' + toSwiftColor(styling.placeholderColor) + ',');
    stylingLines.push('  cornerRadius: ' + styling.cornerRadius + ',');
    stylingLines.push('  borderWidth: ' + styling.borderWidth + ',');
    stylingLines.push('  padding: EdgeInsets(top: ' + styling.paddingV + ', leading: ' + styling.paddingH + ', bottom: ' + styling.paddingV + ', trailing: ' + styling.paddingH + '),');
    stylingLines.push('  fieldHeight: ' + styling.fieldHeight);
    stylingLines.push(')');
    stylingCode = stylingLines.join('\n');
  }
  var styParam = showStyling ? 'fieldStyling' : 'nil';
  var getLabel = function (f) {
    return fieldLabels && fieldLabels[f.id] || f.label;
  };
  var getPlaceholder = function (f) {
    return fieldPlaceholders && fieldPlaceholders[f.id] || f.placeholder;
  };
  var hasCustomPlaceholder = function (f) {
    return fieldPlaceholders && fieldPlaceholders[f.id] && fieldPlaceholders[f.id] !== DEFAULT_PLACEHOLDERS[f.id];
  };
  if (activeTab === 'viewbuilder') {
    var lines = [];
    lines.push('if let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self) {');
    lines.push('  cardScope.screen = { scope in');
    lines.push('    AnyView(');
    lines.push('      VStack(spacing: 16) {');
    if (currentLayout) {
      currentLayout.rows.forEach(function (row) {
        var active = row.filter(function (id) {
          return enabled[id];
        });
        if (active.length === 0) return;
        if (active.length === 1) {
          var f = FORM_FIELDS.find(function (ff) {
            return ff.id === active[0];
          });
          if (!f) return;
          if (active[0] === 'submit') {
            lines.push('        Button("' + submitButtonText + '") { scope.submit() }');
            lines.push('          .buttonStyle(.borderedProminent)');
            lines.push('          .frame(maxWidth: .infinity)');
          } else {
            lines.push('        scope.' + f.iosMethod + '(label: "' + getLabel(f) + '", styling: ' + styParam + ')');
          }
        } else {
          lines.push('        HStack(spacing: 12) {');
          active.forEach(function (id) {
            var f = FORM_FIELDS.find(function (ff) {
              return ff.id === id;
            });
            if (!f) return;
            if (id === 'submit') {
              lines.push('          Button("' + submitButtonText + '") { scope.submit() }');
              lines.push('            .buttonStyle(.borderedProminent)');
            } else {
              lines.push('          scope.' + f.iosMethod + '(label: "' + getLabel(f) + '", styling: ' + styParam + ')');
            }
          });
          lines.push('        }');
        }
      });
    }
    lines.push('      }');
    lines.push('      .padding()');
    lines.push('    )');
    lines.push('  }');
    lines.push('}');
    if (showStyling) return stylingCode + '\n\n' + lines.join('\n');
    return lines.join('\n');
  }
  if (activeTab === 'fieldconfig') {
    var lines = [];
    if (showStyling) {
      lines.push(stylingCode);
      lines.push('');
    }
    lines.push('if let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self) {');
    var configMap = {
      cardNumber: 'cardNumberConfig',
      expiry: 'expiryDateConfig',
      cvv: 'cvvConfig',
      cardholder: 'cardholderNameConfig'
    };
    FORM_FIELDS.forEach(function (f) {
      if (!enabled[f.id] || !configMap[f.id]) return;
      var cfgParts = ['label: "' + getLabel(f) + '"'];
      if (hasCustomPlaceholder(f)) cfgParts.push('placeholder: "' + getPlaceholder(f) + '"');
      if (showStyling) cfgParts.push('styling: fieldStyling');
      lines.push('  cardScope.' + configMap[f.id] + ' = InputFieldConfig(' + cfgParts.join(', ') + ')');
    });
    if (enabled.submit) {
      lines.push('  cardScope.submitButtonText = "' + submitButtonText + '"');
    }
    lines.push('}');
    return lines.join('\n');
  }
  var themeCode = '';
  if (showCheckoutTheme) {
    var ct = checkoutTheme;
    var colorLines = [];
    if (ct.primerColorBrand !== '#2F98FF') colorLines.push('      primerColorBrand: ' + toSwiftColor(ct.primerColorBrand));
    if (ct.primerColorBackground !== '#FFFFFF') colorLines.push('      primerColorBackground: ' + toSwiftColor(ct.primerColorBackground));
    if (ct.primerColorTextPrimary !== '#1C1B18') colorLines.push('      primerColorTextPrimary: ' + toSwiftColor(ct.primerColorTextPrimary));
    if (ct.primerColorTextSecondary !== '#5C5953') colorLines.push('      primerColorTextSecondary: ' + toSwiftColor(ct.primerColorTextSecondary));
    if (ct.primerColorTextPlaceholder !== '#9D9A92') colorLines.push('      primerColorTextPlaceholder: ' + toSwiftColor(ct.primerColorTextPlaceholder));
    if (ct.primerColorTextDisabled !== '#C5C2BA') colorLines.push('      primerColorTextDisabled: ' + toSwiftColor(ct.primerColorTextDisabled));
    if (ct.primerColorBorderOutlinedDefault !== '#E4E2DD') colorLines.push('      primerColorBorderOutlinedDefault: ' + toSwiftColor(ct.primerColorBorderOutlinedDefault));
    if (ct.primerColorBorderOutlinedFocus !== '#2F98FF') colorLines.push('      primerColorBorderOutlinedFocus: ' + toSwiftColor(ct.primerColorBorderOutlinedFocus));
    if (ct.primerColorBorderOutlinedError !== '#C44040') colorLines.push('      primerColorBorderOutlinedError: ' + toSwiftColor(ct.primerColorBorderOutlinedError));
    if (ct.primerColorFocus !== '#2F98FF') colorLines.push('      primerColorFocus: ' + toSwiftColor(ct.primerColorFocus));
    if (ct.primerColorRed500 !== '#C44040') colorLines.push('      primerColorRed500: ' + toSwiftColor(ct.primerColorRed500));
    if (ct.primerColorGreen500 !== '#1A8A5C') colorLines.push('      primerColorGreen500: ' + toSwiftColor(ct.primerColorGreen500));
    var radiusLines = [];
    if (ct.primerRadiusMedium !== 8) radiusLines.push('      primerRadiusMedium: ' + ct.primerRadiusMedium);
    if (ct.primerRadiusSmall !== 4) radiusLines.push('      primerRadiusSmall: ' + ct.primerRadiusSmall);
    var bwLines = [];
    if (ct.primerBorderWidthThin !== 1) bwLines.push('      primerBorderWidthThin: ' + ct.primerBorderWidthThin);
    if (ct.primerBorderWidthMedium !== 2) bwLines.push('      primerBorderWidthMedium: ' + ct.primerBorderWidthMedium);
    var themeParts = [];
    if (colorLines.length) themeParts.push('    colors: ColorOverrides(\n' + colorLines.join(',\n') + '\n    )');
    if (radiusLines.length) themeParts.push('    radius: RadiusOverrides(\n' + radiusLines.join(',\n') + '\n    )');
    if (bwLines.length) themeParts.push('    borderWidth: BorderWidthOverrides(\n' + bwLines.join(',\n') + '\n    )');
    if (themeParts.length) themeCode = '  primerTheme: PrimerCheckoutTheme(\n' + themeParts.join(',\n') + '\n  ),';
  }
  var viewLines = [];
  viewLines.push('PrimerCheckout(');
  viewLines.push('  clientToken: clientToken,');
  if (themeCode) viewLines.push(themeCode);
  viewLines.push('  scope: { checkoutScope in');
  if (showStyling) {
    viewLines.push('    ' + stylingCode.split('\n').join('\n    '));
    viewLines.push('');
  }
  viewLines.push('    if let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self) {');
  viewLines.push('      cardScope.screen = { scope in');
  viewLines.push('        AnyView(');
  viewLines.push('          VStack(spacing: 16) {');
  if (currentLayout) {
    currentLayout.rows.forEach(function (row) {
      var active = row.filter(function (id) {
        return enabled[id];
      });
      if (active.length === 0) return;
      if (active.length === 1) {
        var f = FORM_FIELDS.find(function (ff) {
          return ff.id === active[0];
        });
        if (!f) return;
        if (active[0] === 'submit') {
          viewLines.push('            Button("' + submitButtonText + '") { scope.submit() }');
          viewLines.push('              .buttonStyle(.borderedProminent)');
          viewLines.push('              .frame(maxWidth: .infinity)');
        } else {
          viewLines.push('            scope.' + f.iosMethod + '(label: "' + getLabel(f) + '", styling: ' + styParam + ')');
        }
      } else {
        viewLines.push('            HStack(spacing: 12) {');
        active.forEach(function (id) {
          var f = FORM_FIELDS.find(function (ff) {
            return ff.id === id;
          });
          if (!f) return;
          if (id === 'submit') {
            viewLines.push('              Button("' + submitButtonText + '") { scope.submit() }');
            viewLines.push('                .buttonStyle(.borderedProminent)');
          } else {
            viewLines.push('              scope.' + f.iosMethod + '(label: "' + getLabel(f) + '", styling: ' + styParam + ')');
          }
        });
        viewLines.push('            }');
      }
    });
  }
  viewLines.push('          }');
  viewLines.push('          .padding()');
  viewLines.push('        )');
  viewLines.push('      }');
  viewLines.push('    }');
  viewLines.push('  }');
  viewLines.push(')');
  return viewLines.join('\n');
};

export const getAndroidCode = (activeTab, layout, enabled, showTheme, theme) => {
  const currentLayout = LAYOUTS.find(l => l.id === layout);
  var themeCode = '';
  if (showTheme) {
    var tl = [];
    var toHex = function (c) {
      return '0xFF' + c.replace('#', '').toUpperCase();
    };
    tl.push('val primerTheme = PrimerTheme(');
    tl.push('    lightColorTokens = object : LightColorTokens() {');
    tl.push('        override val primerColorBrand: Color = Color(' + toHex(theme.buttonColor) + ')');
    tl.push('        override val primerColorBackground: Color = Color(' + toHex(theme.backgroundColor) + ')');
    tl.push('        override val primerColorTextPrimary: Color = Color(' + toHex(theme.labelColor) + ')');
    tl.push('        override val primerColorTextPlaceholder: Color = Color(' + toHex(theme.placeholderColor) + ')');
    tl.push('        override val primerColorBorderOutlinedDefault: Color = Color(' + toHex(theme.borderColor) + ')');
    tl.push('        override val primerColorBorderOutlinedHover: Color = Color(' + toHex(theme.focusedBorderColor) + ')');
    tl.push('        override val primerColorBorderOutlinedError: Color = Color(' + toHex(theme.errorBorderColor) + ')');
    tl.push('    },');
    tl.push('    radiusTokens = RadiusTokens(');
    tl.push('        medium = ' + theme.cornerRadius + '.dp,');
    tl.push('    ),');
    tl.push('    borderWidthTokens = BorderWidthTokens(');
    tl.push('        thin = ' + theme.borderWidth + '.dp,');
    tl.push('    ),');
    tl.push(')');
    themeCode = tl.join('\n');
  }
  if (activeTab === 'compose') {
    var ch = enabled.cardholder;
    var sub = enabled.submit;
    var lines = [];
    if (showTheme) {
      lines.push(themeCode);
      lines.push('');
    }
    lines.push('val checkout = rememberPrimerCheckoutController(clientToken, settings)');
    lines.push('val controller = rememberCardFormController(checkout)');
    lines.push('');
    if (showTheme) {
      lines.push('// Pass theme to PrimerCheckoutSheet or PrimerCheckoutHost:');
      lines.push('// PrimerCheckoutSheet(checkout = checkout, theme = primerTheme, ...)');
      lines.push('');
    }
    if (layout === 'grouped') {
      lines.push('PrimerCardForm(');
      lines.push('    controller = controller,');
      if (!ch) {
        lines.push('    cardDetails = {');
        lines.push('        CardFormDefaults.CardDetailsContent(');
        lines.push('            controller = controller,');
        lines.push('            cardholderName = {},');
        lines.push('        )');
        lines.push('    },');
      }
      if (!sub) lines.push('    submitButton = {},');
      lines.push(')');
    } else if (layout === 'vertical') {
      lines.push('PrimerCardForm(');
      lines.push('    controller = controller,');
      lines.push('    cardDetails = {');
      lines.push('        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {');
      lines.push('            CardFormDefaults.CardNumberField(controller)');
      lines.push('            CardFormDefaults.ExpiryField(controller)');
      lines.push('            CardFormDefaults.CvvField(controller)');
      if (ch) lines.push('            CardFormDefaults.CardholderField(controller)');
      lines.push('        }');
      lines.push('    },');
      if (!sub) lines.push('    submitButton = {},');
      lines.push(')');
    } else if (layout === 'compact') {
      lines.push('PrimerCardForm(');
      lines.push('    controller = controller,');
      lines.push('    cardDetails = {');
      lines.push('        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {');
      lines.push('            CardFormDefaults.CardNumberField(controller)');
      lines.push('            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {');
      lines.push('                CardFormDefaults.ExpiryField(controller, modifier = Modifier.weight(1f))');
      lines.push('                CardFormDefaults.CvvField(controller, modifier = Modifier.weight(1f))');
      lines.push('            }');
      if (ch && sub) {
        lines.push('            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {');
        lines.push('                CardFormDefaults.CardholderField(controller, modifier = Modifier.weight(1f))');
        lines.push('                CardFormDefaults.SubmitButton(controller, modifier = Modifier.weight(1f))');
        lines.push('            }');
      } else if (ch) {
        lines.push('            CardFormDefaults.CardholderField(controller)');
      } else if (sub) {
        lines.push('            CardFormDefaults.SubmitButton(controller)');
      }
      lines.push('        }');
      lines.push('    },');
      lines.push('    submitButton = {},');
      lines.push(')');
    } else if (layout === 'single-line') {
      lines.push('PrimerCardForm(');
      lines.push('    controller = controller,');
      lines.push('    cardDetails = {');
      lines.push('        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {');
      lines.push('            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {');
      lines.push('                CardFormDefaults.CardNumberField(controller, modifier = Modifier.weight(2f))');
      lines.push('                CardFormDefaults.ExpiryField(controller, modifier = Modifier.weight(1f))');
      lines.push('                CardFormDefaults.CvvField(controller, modifier = Modifier.weight(1f))');
      lines.push('            }');
      if (ch) lines.push('            CardFormDefaults.CardholderField(controller)');
      lines.push('        }');
      lines.push('    },');
      if (!sub) lines.push('    submitButton = {},');
      lines.push(')');
    }
    return lines.join('\n');
  }
  return '';
};

export const CardFormLayoutBuilder = () => {
  const [platform, setPlatform] = useState('web');
  const [layout, setLayout] = useState('grouped');
  const [enabled, setEnabled] = useState(ENABLED_DEFAULTS);
  const [hideLabels, setHideLabels] = useState(false);
  const [isDisabled, setIsDisabled] = useState(false);
  const [cardholderVisible, setCardholderVisible] = useState(true);
  const [cardholderRequired, setCardholderRequired] = useState(false);
  const [showAmount, setShowAmount] = useState(false);
  const [buttonText, setButtonText] = useState('Pay Now');
  const [showStyling, setShowStyling] = useState(false);
  const [styling, setStyling] = useState(IOS_STYLING_DEFAULTS);
  const [submitButtonText, setSubmitButtonText] = useState('Pay now');
  const [showCheckoutTheme, setShowCheckoutTheme] = useState(false);
  const [checkoutTheme, setCheckoutTheme] = useState(IOS_CHECKOUT_THEME_DEFAULTS);
  const [showTheme, setShowTheme] = useState(false);
  const [theme, setTheme] = useState(ANDROID_THEME_DEFAULTS);
  const [fieldLabels, setFieldLabels] = useState(DEFAULT_LABELS);
  const [fieldPlaceholders, setFieldPlaceholders] = useState(DEFAULT_PLACEHOLDERS);
  const [cardholderDefault, setCardholderDefault] = useState('');
  const [showWebStyles, setShowWebStyles] = useState(false);
  const [webStyles, setWebStyles] = useState(WEB_STYLE_DEFAULTS);
  const [activeTab, setActiveTab] = useState('html');
  const currentLayout = useMemo(() => LAYOUTS.find(l => l.id === layout), [layout]);
  const switchPlatform = p => {
    setPlatform(p);
    if (p === 'web') setActiveTab('html');
    if (p === 'ios') setActiveTab('viewbuilder');
    if (p === 'android') setActiveTab('compose');
  };
  const toggleField = id => {
    var f = FORM_FIELDS.find(function (ff) {
      return ff.id === id;
    });
    if (f && f.required) return;
    setEnabled(prev => Object.assign({}, prev, {
      [id]: !prev[id]
    }));
  };
  const updateStyling = (key, value) => {
    setStyling(prev => Object.assign({}, prev, {
      [key]: value
    }));
  };
  const updateTheme = (key, value) => {
    setTheme(prev => Object.assign({}, prev, {
      [key]: value
    }));
  };
  const updateCheckoutTheme = (key, value) => {
    setCheckoutTheme(prev => Object.assign({}, prev, {
      [key]: value
    }));
  };
  const updateWebStyles = (key, value) => {
    setWebStyles(prev => Object.assign({}, prev, {
      [key]: value
    }));
  };
  const updateLabel = (id, value) => {
    setFieldLabels(prev => Object.assign({}, prev, {
      [id]: value
    }));
  };
  const updatePlaceholder = (id, value) => {
    setFieldPlaceholders(prev => Object.assign({}, prev, {
      [id]: value
    }));
  };
  const resetAll = () => {
    setLayout('grouped');
    setEnabled(ENABLED_DEFAULTS);
    setFieldLabels(DEFAULT_LABELS);
    setFieldPlaceholders(DEFAULT_PLACEHOLDERS);
    if (platform === 'web') {
      setHideLabels(false);
      setIsDisabled(false);
      setCardholderVisible(true);
      setCardholderRequired(false);
      setShowAmount(false);
      setButtonText('Pay Now');
      setCardholderDefault('');
      setShowWebStyles(false);
      setWebStyles(WEB_STYLE_DEFAULTS);
    } else if (platform === 'ios') {
      setShowStyling(false);
      setStyling(IOS_STYLING_DEFAULTS);
      setSubmitButtonText('Pay now');
      setShowCheckoutTheme(false);
      setCheckoutTheme(IOS_CHECKOUT_THEME_DEFAULTS);
    } else {
      setShowTheme(false);
      setTheme(ANDROID_THEME_DEFAULTS);
    }
  };
  const codeTabs = useMemo(() => {
    if (platform === 'web') return [{
      id: 'html',
      label: 'HTML'
    }, {
      id: 'css',
      label: 'CSS'
    }, {
      id: 'js',
      label: 'Events (JS)'
    }];
    if (platform === 'ios') return [{
      id: 'viewbuilder',
      label: 'ViewBuilder'
    }, {
      id: 'fieldconfig',
      label: 'InputFieldConfig'
    }, {
      id: 'full',
      label: 'Full Init'
    }];
    return [{
      id: 'compose',
      label: 'Compose'
    }];
  }, [platform]);
  const getCode = useCallback(() => {
    if (platform === 'web') return getWebCode(activeTab, layout, enabled, hideLabels, isDisabled, cardholderVisible, cardholderRequired, showAmount, buttonText, fieldLabels, fieldPlaceholders, cardholderDefault, showWebStyles, webStyles);
    if (platform === 'ios') return getIosCode(activeTab, layout, enabled, showStyling, styling, submitButtonText, fieldLabels, fieldPlaceholders, showCheckoutTheme, checkoutTheme);
    return getAndroidCode(activeTab, layout, enabled, showTheme, theme);
  }, [platform, activeTab, layout, enabled, hideLabels, isDisabled, cardholderVisible, cardholderRequired, showAmount, buttonText, showStyling, styling, submitButtonText, showTheme, theme, fieldLabels, fieldPlaceholders, cardholderDefault, showCheckoutTheme, checkoutTheme, showWebStyles, webStyles]);
  const webCardholderEnabled = platform === 'web' ? cardholderVisible : enabled.cardholder;
  const renderPreview = () => {
    var effectiveEnabled = Object.assign({}, enabled);
    if (platform === 'web') effectiveEnabled.cardholder = cardholderVisible;
    var previewBg = platform === 'ios' && showCheckoutTheme ? checkoutTheme.primerColorBackground : platform === 'web' && showWebStyles ? webStyles.primerColorBackgroundPrimary : platform === 'ios' ? '#F2F2F7' : platform === 'android' ? '#FAFAFA' : DS.color.bgPage;
    var cardBg = platform === 'ios' && showCheckoutTheme ? checkoutTheme.primerColorBackground : platform === 'web' && showWebStyles ? webStyles.primerColorBackgroundPrimary : '#FFFFFF';
    var cardRadius = platform === 'ios' ? 12 : platform === 'android' ? 12 : DS.radius.lg;
    var cardPadding = platform === 'ios' ? 20 : platform === 'android' ? 16 : DS.space.xl;
    var cardShadow = platform === 'ios' ? '0 1px 4px rgba(0,0,0,0.08)' : platform === 'android' ? '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.06)' : '0 2px 12px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04)';
    var cardFont = platform === 'ios' ? '-apple-system, system-ui, sans-serif' : platform === 'android' ? 'Roboto, sans-serif' : undefined;
    var fieldGap = platform === 'ios' ? 12 : platform === 'android' ? 8 : showWebStyles ? webStyles.primerSpaceSmall : DS.space.md;
    var rowGap = platform === 'ios' ? 8 : platform === 'android' ? 8 : DS.space.sm;
    var submitBtnStyle = {};
    if (platform === 'web') {
      submitBtnStyle = {
        flex: 1,
        padding: DS.space.md + 'px ' + DS.space.xl + 'px',
        border: 'none',
        borderRadius: showWebStyles ? webStyles.primerRadiusMedium : DS.radius.md,
        fontSize: DS.font.lg,
        fontWeight: 600,
        cursor: 'default',
        background: showWebStyles ? webStyles.primerColorBrand : DS.color.brand,
        color: '#fff'
      };
    } else if (platform === 'ios') {
      submitBtnStyle = {
        flex: 1,
        padding: '14px 20px',
        border: 'none',
        borderRadius: showStyling ? styling.cornerRadius : 10,
        fontSize: 17,
        fontWeight: 600,
        cursor: 'default',
        background: showCheckoutTheme ? checkoutTheme.primerColorBrand : '#2F98FF',
        color: '#fff',
        height: showStyling ? styling.fieldHeight : 48,
        fontFamily: '-apple-system, system-ui, sans-serif'
      };
    } else {
      submitBtnStyle = {
        flex: 1,
        padding: '16px 24px',
        border: 'none',
        borderRadius: showTheme ? theme.buttonCornerRadius : 8,
        fontSize: 14,
        fontWeight: 500,
        cursor: 'default',
        background: showTheme ? theme.buttonColor : '#2F98FF',
        color: showTheme ? theme.buttonTextColor : '#fff',
        height: 56,
        letterSpacing: 0.5,
        fontFamily: 'Roboto, sans-serif',
        textTransform: 'uppercase'
      };
    }
    var submitLabel = platform === 'ios' ? submitButtonText : platform === 'web' ? buttonText : 'Pay Now';
    return <div style={{
      padding: DS.space.xxl,
      display: 'flex',
      justifyContent: 'center',
      background: previewBg,
      minHeight: 280
    }}>
        <div style={{
      width: '100%',
      maxWidth: 420
    }}>
          <div style={{
      background: cardBg,
      borderRadius: cardRadius,
      padding: cardPadding,
      boxShadow: cardShadow,
      fontFamily: cardFont,
      opacity: platform === 'web' && isDisabled ? 0.5 : 1,
      transition: 'opacity ' + DS.transition.normal
    }}>
            <div style={{
      display: 'flex',
      flexDirection: 'column',
      gap: fieldGap
    }}>
              {currentLayout && currentLayout.rows.map((row, ri) => {
      var active = row.filter(function (id) {
        return effectiveEnabled[id];
      });
      if (active.length === 0) return null;
      if (platform === 'web' && layout === 'single-line') {
        var cardFields = ['cardNumber', 'expiry', 'cvv'];
        var isSingleLineCardRow = active.some(function (id) {
          return cardFields.indexOf(id) >= 0;
        }) && active.filter(function (id) {
          return cardFields.indexOf(id) >= 0;
        }).length >= 2;
        if (isSingleLineCardRow) {
          var joinedFields = active.filter(function (id) {
            return cardFields.indexOf(id) >= 0;
          });
          return <div key={ri} style={{
            display: 'flex',
            border: (showWebStyles ? webStyles.primerWidthDefault + 'px' : '1.5px') + ' solid ' + (showWebStyles ? webStyles.primerColorBorderOutlinedDefault : DS.color.border),
            borderRadius: showWebStyles ? webStyles.primerRadiusSmall : DS.radius.md,
            overflow: 'hidden',
            background: showWebStyles ? webStyles.primerColorBackgroundPrimary : DS.color.bgSurface
          }}>
                        {joinedFields.map((id, idx) => {
            var comp = FORM_FIELDS.find(function (c) {
              return c.id === id;
            });
            if (!comp) return null;
            return <div key={id} style={{
              flex: id === 'cardNumber' ? 2 : 1,
              borderRight: idx < joinedFields.length - 1 ? '1px solid ' + (showWebStyles ? webStyles.primerColorBorderOutlinedDefault : DS.color.border) : 'none'
            }}>
                              {!hideLabels && <span style={{
              fontSize: DS.font.xs,
              fontWeight: 500,
              color: showWebStyles ? webStyles.primerColorTextPrimary : DS.color.textSecondary,
              padding: DS.space.sm + 'px ' + DS.space.md + 'px 0',
              display: 'block'
            }}>{fieldLabels[id] || comp.label}</span>}
                              <div style={{
              padding: DS.space.xs + 2 + 'px ' + DS.space.md + 'px ' + (DS.space.sm + 2) + 'px',
              fontSize: DS.font.lg,
              color: showWebStyles ? webStyles.primerColorTextPlaceholder : DS.color.textDisabled,
              display: 'flex',
              alignItems: 'center',
              gap: DS.space.sm
            }}>
                                {id === 'cardNumber' && <span style={{
              fontSize: DS.font.xl
            }}></span>}
                                {fieldPlaceholders[id] || comp.placeholder}
                              </div>
                            </div>;
          })}
                      </div>;
        }
      }
      return <div key={ri} style={{
        display: 'flex',
        gap: rowGap
      }}>
                    {active.map(id => {
        var f = FORM_FIELDS.find(function (ff) {
          return ff.id === id;
        });
        if (!f) return null;
        if (id === 'submit') return <button key={id} style={submitBtnStyle}>{submitLabel}</button>;
        if (platform === 'web') {
          var webPlaceholder = id === 'cardholder' && cardholderDefault ? cardholderDefault : fieldPlaceholders[id] || f.placeholder;
          return <div key={id} style={{
            flex: 1
          }}>
                            <SimInputWeb label={fieldLabels[id] || f.label} placeholder={webPlaceholder} hideLabels={hideLabels} hasIcon={id === 'cardNumber'} styles={showWebStyles ? webStyles : null} />
                          </div>;
        }
        if (platform === 'ios') {
          var iosSty = showStyling ? styling : {
            cornerRadius: 10,
            fieldHeight: 48,
            fontSize: 16,
            borderColor: '#E4E2DD',
            backgroundColor: '#FFFFFF',
            labelFontSize: 11,
            labelColor: '#8E8E93',
            placeholderColor: '#C7C7CC',
            borderWidth: 1,
            paddingV: 6,
            paddingH: 14,
            textColor: '#1C1B18',
            fontWeight: 400
          };
          if (showCheckoutTheme) {
            iosSty = Object.assign({}, iosSty, {
              borderColor: checkoutTheme.primerColorBorderOutlinedDefault,
              backgroundColor: checkoutTheme.primerColorBackground,
              labelColor: checkoutTheme.primerColorTextSecondary,
              placeholderColor: checkoutTheme.primerColorTextPlaceholder,
              textColor: checkoutTheme.primerColorTextPrimary
            });
          }
          return <div key={id} style={{
            flex: 1
          }}>
                            <SimInputIos label={fieldLabels[id] || f.label} placeholder={fieldPlaceholders[id] || f.placeholder} hasIcon={id === 'cardNumber'} styling={iosSty} />
                          </div>;
        }
        var androidFlex = layout === 'single-line' && id === 'cardNumber' ? 2 : 1;
        return <div key={id} style={{
          flex: androidFlex,
          minWidth: 0
        }}>
                          <SimInputAndroid label={f.label} placeholder={f.placeholder} hasIcon={false} theme={showTheme ? theme : {
          inputMode: 'OUTLINED',
          cornerRadius: 8,
          backgroundColor: '#FFFFFF',
          borderColor: '#E0E0E0',
          borderWidth: 1,
          labelColor: '#757575',
          placeholderColor: '#9E9E9E'
        }} />
                        </div>;
      })}
                  </div>;
    })}
            </div>
          </div>
        </div>
      </div>;
  };
  var fieldsToShow = FORM_FIELDS;
  if (platform === 'web') {
    fieldsToShow = FORM_FIELDS.filter(function (f) {
      return f.id !== 'cardholder';
    });
  }
  return <div style={{
    display: 'grid',
    gridTemplateColumns: '280px 1fr',
    gap: DS.space.xl,
    alignItems: 'start'
  }}>
      <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: DS.space.md
  }}>
        <Panel title="Platform">
          <div style={{
    padding: DS.space.sm,
    display: 'flex',
    gap: DS.space.xs
  }}>
            {['web', 'ios', 'android'].map(p => <HoverButton key={p} active={platform === p} onClick={() => switchPlatform(p)} style={{
    flex: 1,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: DS.space.xs,
    padding: DS.space.sm + 1 + 'px ' + DS.space.sm + 'px',
    fontSize: DS.font.xs,
    fontWeight: 600
  }}>
                <span style={{
    color: platform === p ? DS.color.brand : DS.color.textMuted
  }}>{PLATFORM_ICONS[p]}</span>
                {p === 'web' ? 'Web' : p === 'ios' ? 'iOS' : 'Android'}
              </HoverButton>)}
          </div>
        </Panel>

        <Panel title="Layout Preset">
          <div style={{
    padding: DS.space.sm
  }}>
            {LAYOUTS.map(l => <HoverButton key={l.id} active={layout === l.id} onClick={() => setLayout(l.id)} style={{
    display: 'flex',
    alignItems: 'center',
    gap: DS.space.sm + 2,
    width: '100%',
    padding: DS.space.sm + 'px ' + DS.space.md + 'px',
    textAlign: 'left',
    marginBottom: DS.space.xs
  }}>
                <LayoutIcon layoutId={l.id} active={layout === l.id} />
                <div>
                  <div style={{
    fontSize: DS.font.sm,
    fontWeight: 600,
    color: layout === l.id ? DS.color.brand : DS.color.text
  }}>{l.name}</div>
                  <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    marginTop: 1
  }}>{l.desc}</div>
                </div>
              </HoverButton>)}
          </div>
        </Panel>

        <Panel title="Fields">
          <div style={{
    padding: DS.space.sm
  }}>
            {fieldsToShow.map(f => <div key={f.id} style={{
    display: 'flex',
    alignItems: 'center',
    gap: DS.space.sm,
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    borderRadius: DS.radius.sm
  }}>
                <span style={{
    width: 8,
    height: 8,
    borderRadius: 2,
    background: f.color,
    flexShrink: 0
  }} />
                <span style={{
    fontFamily: DS.font.mono,
    fontSize: DS.font.xs,
    fontWeight: 500,
    color: DS.color.text,
    flex: 1
  }}>{f.label}</span>
                {f.required && <Badge color={DS.color.warning}>REQ</Badge>}
                <Toggle on={enabled[f.id]} onClick={() => toggleField(f.id)} disabled={f.required} label={'Toggle ' + f.label} />
              </div>)}
          </div>
        </Panel>

        {(platform === 'web' || platform === 'ios') && <Panel title="Field Text">
            <div style={{
    padding: DS.space.sm
  }}>
              {FORM_FIELDS.filter(function (f) {
    return f.id !== 'submit';
  }).filter(function (f) {
    return platform === 'web' ? f.id !== 'cardholder' || cardholderVisible : enabled[f.id];
  }).map(f => <div key={f.id} style={{
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    marginBottom: DS.space.sm
  }}>
                  <div style={{
    fontSize: DS.font.xs,
    fontWeight: 600,
    color: DS.color.text,
    marginBottom: DS.space.xs
  }}>{f.label}</div>
                  <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: DS.space.xs
  }}>
                    <div>
                      <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    marginBottom: 2
  }}>Label</div>
                      <TextInput value={fieldLabels[f.id] || ''} onChange={v => updateLabel(f.id, v)} placeholder={f.label} />
                    </div>
                    <div>
                      <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    marginBottom: 2
  }}>Placeholder</div>
                      <TextInput value={fieldPlaceholders[f.id] || ''} onChange={v => updatePlaceholder(f.id, v)} placeholder={f.placeholder} />
                    </div>
                  </div>
                </div>)}
            </div>
          </Panel>}

        {platform === 'web' && <Panel title="Form Options">
            <div style={{
    padding: DS.space.sm
  }}>
              {[{
    label: 'Hide labels',
    desc: 'hide-labels',
    on: hideLabels,
    toggle: function () {
      setHideLabels(function (v) {
        return !v;
      });
    }
  }, {
    label: 'Disabled',
    desc: 'disabled',
    on: isDisabled,
    toggle: function () {
      setIsDisabled(function (v) {
        return !v;
      });
    }
  }, {
    label: 'Cardholder visible',
    desc: 'cardholderName.visible',
    on: cardholderVisible,
    toggle: function () {
      setCardholderVisible(function (v) {
        return !v;
      });
    }
  }].map(opt => <div key={opt.desc} style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px'
  }}>
                  <div>
                    <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>{opt.label}</div>
                    <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    fontFamily: DS.font.mono
  }}>{opt.desc}</div>
                  </div>
                  <Toggle on={opt.on} onClick={opt.toggle} label={opt.label} />
                </div>)}
              {cardholderVisible && <div>
                  <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px'
  }}>
                    <div>
                      <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>Cardholder required</div>
                      <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    fontFamily: DS.font.mono
  }}>cardholderName.required</div>
                    </div>
                    <Toggle on={cardholderRequired} onClick={() => setCardholderRequired(function (v) {
    return !v;
  })} label="Cardholder required" />
                  </div>
                  <div style={{
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    marginTop: DS.space.xs
  }}>
                    <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary,
    marginBottom: DS.space.xs
  }}>Cardholder default value</div>
                    <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    fontFamily: DS.font.mono,
    marginBottom: 2
  }}>cardholderName.defaultValue</div>
                    <TextInput value={cardholderDefault} onChange={setCardholderDefault} placeholder="John Smith" />
                  </div>
                </div>}
              <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px'
  }}>
                <div>
                  <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>Show amount</div>
                  <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    fontFamily: DS.font.mono
  }}>submitButton.amountVisible</div>
                </div>
                <Toggle on={showAmount} onClick={() => setShowAmount(function (v) {
    return !v;
  })} label="Show amount" />
              </div>
              <div style={{
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px'
  }}>
                <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary,
    marginBottom: DS.space.xs
  }}>Button text</div>
                <TextInput value={buttonText} onChange={setButtonText} placeholder="Pay Now" />
              </div>
            </div>
          </Panel>}

        {platform === 'web' && <Panel title="Custom Styles">
            <div style={{
    padding: DS.space.sm
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    marginBottom: DS.space.sm
  }}>
                <span style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>Override design tokens</span>
                <Toggle on={showWebStyles} onClick={() => setShowWebStyles(function (v) {
    return !v;
  })} label="Enable custom styles" />
              </div>
              {showWebStyles && <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: DS.space.md,
    padding: '0 ' + (DS.space.sm + 2) + 'px ' + DS.space.sm + 'px'
  }}>
                  <ColorPicker value={webStyles.primerColorBrand} onChange={v => updateWebStyles('primerColorBrand', v)} label="Brand color" />
                  <ColorPicker value={webStyles.primerColorBackgroundPrimary} onChange={v => updateWebStyles('primerColorBackgroundPrimary', v)} label="Background" />
                  <ColorPicker value={webStyles.primerColorTextPrimary} onChange={v => updateWebStyles('primerColorTextPrimary', v)} label="Text primary" />
                  <ColorPicker value={webStyles.primerColorTextPlaceholder} onChange={v => updateWebStyles('primerColorTextPlaceholder', v)} label="Text placeholder" />
                  <ColorPicker value={webStyles.primerColorBorderOutlinedDefault} onChange={v => updateWebStyles('primerColorBorderOutlinedDefault', v)} label="Border default" />
                  <ColorPicker value={webStyles.primerColorBorderOutlinedFocus} onChange={v => updateWebStyles('primerColorBorderOutlinedFocus', v)} label="Border focus" />
                  <ColorPicker value={webStyles.primerColorBorderOutlinedError} onChange={v => updateWebStyles('primerColorBorderOutlinedError', v)} label="Border error" />
                  <ColorPicker value={webStyles.primerColorFocus} onChange={v => updateWebStyles('primerColorFocus', v)} label="Focus ring" />
                  <ColorPicker value={webStyles.primerColorTextNegative} onChange={v => updateWebStyles('primerColorTextNegative', v)} label="Error text" />
                  <Slider min={0} max={24} value={webStyles.primerRadiusSmall} onChange={v => updateWebStyles('primerRadiusSmall', v)} label="Input radius" unit="px" />
                  <Slider min={0} max={24} value={webStyles.primerRadiusMedium} onChange={v => updateWebStyles('primerRadiusMedium', v)} label="Button radius" unit="px" />
                  <Slider min={0} max={4} value={webStyles.primerWidthDefault} onChange={v => updateWebStyles('primerWidthDefault', v)} label="Border width" unit="px" />
                  <Slider min={0} max={4} value={webStyles.primerWidthFocus} onChange={v => updateWebStyles('primerWidthFocus', v)} label="Focus width" unit="px" />
                  <Slider min={0} max={24} value={webStyles.primerSpaceSmall} onChange={v => updateWebStyles('primerSpaceSmall', v)} label="Field gap" unit="px" />
                </div>}
            </div>
          </Panel>}

        {platform === 'ios' && <Panel title="Field Styling">
            <div style={{
    padding: DS.space.sm
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    marginBottom: DS.space.sm
  }}>
                <span style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>Custom PrimerFieldStyling</span>
                <Toggle on={showStyling} onClick={() => setShowStyling(function (v) {
    return !v;
  })} label="Enable styling" />
              </div>
              {showStyling && <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: DS.space.md,
    padding: '0 ' + (DS.space.sm + 2) + 'px ' + DS.space.sm + 'px'
  }}>
                  <Slider min={0} max={24} value={styling.cornerRadius} onChange={v => updateStyling('cornerRadius', v)} label="Corner radius" unit="pt" />
                  <Slider min={12} max={24} value={styling.fontSize} onChange={v => updateStyling('fontSize', v)} label="Font size" unit="pt" />
                  <Slider min={36} max={64} value={styling.fieldHeight} onChange={v => updateStyling('fieldHeight', v)} label="Field height" unit="pt" />
                  <Slider min={0} max={4} value={styling.borderWidth} onChange={v => updateStyling('borderWidth', v)} label="Border width" unit="pt" />
                  <Slider min={8} max={24} value={styling.labelFontSize} onChange={v => updateStyling('labelFontSize', v)} label="Label font size" unit="pt" />
                  <Slider min={0} max={20} value={styling.paddingV} onChange={v => updateStyling('paddingV', v)} label="Padding vertical" unit="pt" />
                  <Slider min={0} max={20} value={styling.paddingH} onChange={v => updateStyling('paddingH', v)} label="Padding horizontal" unit="pt" />
                  <ColorPicker value={styling.borderColor} onChange={v => updateStyling('borderColor', v)} label="Border color" />
                  <ColorPicker value={styling.focusedBorderColor} onChange={v => updateStyling('focusedBorderColor', v)} label="Focus color" />
                  <ColorPicker value={styling.backgroundColor} onChange={v => updateStyling('backgroundColor', v)} label="Background color" />
                  <ColorPicker value={styling.labelColor} onChange={v => updateStyling('labelColor', v)} label="Label color" />
                  <ColorPicker value={styling.placeholderColor} onChange={v => updateStyling('placeholderColor', v)} label="Placeholder color" />
                  <ColorPicker value={styling.errorBorderColor} onChange={v => updateStyling('errorBorderColor', v)} label="Error border color" />
                  <ColorPicker value={styling.textColor} onChange={v => updateStyling('textColor', v)} label="Text color" />
                  <Slider min={100} max={900} step={100} value={styling.fontWeight} onChange={v => updateStyling('fontWeight', v)} label="Font weight" unit="" />
                  <Slider min={100} max={900} step={100} value={styling.labelFontWeight} onChange={v => updateStyling('labelFontWeight', v)} label="Label font weight" unit="" />
                  <div>
                    <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary,
    marginBottom: DS.space.xs
  }}>Font name</div>
                    <TextInput value={styling.fontName} onChange={v => updateStyling('fontName', v)} placeholder="System default" />
                  </div>
                  <div>
                    <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary,
    marginBottom: DS.space.xs
  }}>Label font name</div>
                    <TextInput value={styling.labelFontName} onChange={v => updateStyling('labelFontName', v)} placeholder="System default" />
                  </div>
                </div>}
              <div style={{
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    marginTop: DS.space.sm
  }}>
                <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary,
    marginBottom: DS.space.xs
  }}>Submit button text</div>
                <TextInput value={submitButtonText} onChange={setSubmitButtonText} placeholder="Pay now" />
              </div>
            </div>
          </Panel>}

        {platform === 'ios' && <Panel title="Checkout Theme">
            <div style={{
    padding: DS.space.sm
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    marginBottom: DS.space.sm
  }}>
                <span style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>Custom PrimerCheckoutTheme</span>
                <Toggle on={showCheckoutTheme} onClick={() => setShowCheckoutTheme(function (v) {
    return !v;
  })} label="Enable checkout theme" />
              </div>
              {showCheckoutTheme && <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: DS.space.md,
    padding: '0 ' + (DS.space.sm + 2) + 'px ' + DS.space.sm + 'px'
  }}>
                  <ColorPicker value={checkoutTheme.primerColorBrand} onChange={v => updateCheckoutTheme('primerColorBrand', v)} label="Brand color" />
                  <ColorPicker value={checkoutTheme.primerColorBackground} onChange={v => updateCheckoutTheme('primerColorBackground', v)} label="Background" />
                  <ColorPicker value={checkoutTheme.primerColorTextPrimary} onChange={v => updateCheckoutTheme('primerColorTextPrimary', v)} label="Text primary" />
                  <ColorPicker value={checkoutTheme.primerColorTextSecondary} onChange={v => updateCheckoutTheme('primerColorTextSecondary', v)} label="Text secondary" />
                  <ColorPicker value={checkoutTheme.primerColorTextPlaceholder} onChange={v => updateCheckoutTheme('primerColorTextPlaceholder', v)} label="Text placeholder" />
                  <ColorPicker value={checkoutTheme.primerColorTextDisabled} onChange={v => updateCheckoutTheme('primerColorTextDisabled', v)} label="Text disabled" />
                  <ColorPicker value={checkoutTheme.primerColorBorderOutlinedDefault} onChange={v => updateCheckoutTheme('primerColorBorderOutlinedDefault', v)} label="Border default" />
                  <ColorPicker value={checkoutTheme.primerColorBorderOutlinedFocus} onChange={v => updateCheckoutTheme('primerColorBorderOutlinedFocus', v)} label="Border focus" />
                  <ColorPicker value={checkoutTheme.primerColorBorderOutlinedError} onChange={v => updateCheckoutTheme('primerColorBorderOutlinedError', v)} label="Border error" />
                  <ColorPicker value={checkoutTheme.primerColorFocus} onChange={v => updateCheckoutTheme('primerColorFocus', v)} label="Focus ring" />
                  <ColorPicker value={checkoutTheme.primerColorRed500} onChange={v => updateCheckoutTheme('primerColorRed500', v)} label="Error color" />
                  <ColorPicker value={checkoutTheme.primerColorGreen500} onChange={v => updateCheckoutTheme('primerColorGreen500', v)} label="Success color" />
                  <Slider min={0} max={24} value={checkoutTheme.primerRadiusMedium} onChange={v => updateCheckoutTheme('primerRadiusMedium', v)} label="Radius medium" unit="pt" />
                  <Slider min={0} max={16} value={checkoutTheme.primerRadiusSmall} onChange={v => updateCheckoutTheme('primerRadiusSmall', v)} label="Radius small" unit="pt" />
                  <Slider min={0} max={4} value={checkoutTheme.primerBorderWidthThin} onChange={v => updateCheckoutTheme('primerBorderWidthThin', v)} label="Border width thin" unit="pt" />
                  <Slider min={0} max={6} value={checkoutTheme.primerBorderWidthMedium} onChange={v => updateCheckoutTheme('primerBorderWidthMedium', v)} label="Border width medium" unit="pt" />
                </div>}
            </div>
          </Panel>}

        {platform === 'android' && <Panel title="Theme">
            <div style={{
    padding: DS.space.sm
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: DS.space.sm - 1 + 'px ' + (DS.space.sm + 2) + 'px',
    marginBottom: DS.space.sm
  }}>
                <span style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary
  }}>Custom PrimerTheme</span>
                <Toggle on={showTheme} onClick={() => setShowTheme(function (v) {
    return !v;
  })} label="Enable theme" />
              </div>
              {showTheme && <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: DS.space.md,
    padding: '0 ' + (DS.space.sm + 2) + 'px ' + DS.space.sm + 'px'
  }}>
                  <div>
                    <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary,
    marginBottom: DS.space.xs
  }}>Input mode</div>
                    <div style={{
    display: 'flex',
    gap: DS.space.xs
  }}>
                      {['OUTLINED', 'UNDERLINED'].map(m => <HoverButton key={m} active={theme.inputMode === m} onClick={() => updateTheme('inputMode', m)} style={{
    flex: 1,
    padding: DS.space.xs + 1 + 'px ' + DS.space.sm + 'px',
    fontSize: DS.font.xxs,
    fontWeight: 600,
    textAlign: 'center'
  }}>
                          {m}
                        </HoverButton>)}
                    </div>
                  </div>
                  <Slider min={0} max={24} value={theme.cornerRadius} onChange={v => updateTheme('cornerRadius', v)} label="Input corner radius" unit="dp" />
                  <Slider min={0} max={4} value={theme.borderWidth} onChange={v => updateTheme('borderWidth', v)} label="Input border width" unit="dp" />
                  <Slider min={0} max={24} value={theme.buttonCornerRadius} onChange={v => updateTheme('buttonCornerRadius', v)} label="Button corner radius" unit="dp" />
                  <ColorPicker value={theme.backgroundColor} onChange={v => updateTheme('backgroundColor', v)} label="Input background" />
                  <ColorPicker value={theme.borderColor} onChange={v => updateTheme('borderColor', v)} label="Input border color" />
                  <ColorPicker value={theme.labelColor} onChange={v => updateTheme('labelColor', v)} label="Label text color" />
                  <ColorPicker value={theme.placeholderColor} onChange={v => updateTheme('placeholderColor', v)} label="Placeholder text color" />
                  <ColorPicker value={theme.buttonColor} onChange={v => updateTheme('buttonColor', v)} label="Submit button color" />
                  <ColorPicker value={theme.focusedBorderColor} onChange={v => updateTheme('focusedBorderColor', v)} label="Focused border color" />
                  <ColorPicker value={theme.errorBorderColor} onChange={v => updateTheme('errorBorderColor', v)} label="Error border color" />
                  <ColorPicker value={theme.inputTextColor} onChange={v => updateTheme('inputTextColor', v)} label="Input text color" />
                  <ColorPicker value={theme.cursorColor} onChange={v => updateTheme('cursorColor', v)} label="Cursor color" />
                  <ColorPicker value={theme.buttonTextColor} onChange={v => updateTheme('buttonTextColor', v)} label="Button text color" />
                </div>}
            </div>
          </Panel>}
        <div style={{
    display: 'flex',
    justifyContent: 'flex-end'
  }}>
          <HoverButton onClick={resetAll} style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    padding: DS.space.xs + 'px ' + DS.space.sm + 'px',
    gap: DS.space.xs,
    display: 'flex',
    alignItems: 'center'
  }}>
            ↺ Reset to defaults
          </HoverButton>
        </div>
      </div>

      <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: DS.space.lg,
    position: 'sticky',
    top: 100
  }}>
        <Panel title="Live Preview">
          {renderPreview()}
        </Panel>

        <Panel>
          <TabBar tabs={codeTabs} activeTab={activeTab} onTabChange={setActiveTab} extra={<CopyButton getText={getCode} />} />
          <pre style={{
    padding: DS.space.lg,
    fontFamily: DS.font.mono,
    fontSize: DS.font.xs,
    lineHeight: 1.7,
    color: DS.color.textSecondary,
    overflowX: 'auto',
    background: DS.color.bgSurface,
    margin: 0,
    whiteSpace: 'pre',
    tabSize: 2,
    minHeight: 140
  }}>{getCode()}</pre>
        </Panel>
      </div>
    </div>;
};

Design custom card payment form layouts with live preview and platform-specific code generation. Select your platform, choose a layout preset, toggle fields, configure platform-specific options, and copy the generated code.

<CardFormLayoutBuilder />

***

## How to Use

1. **Select a platform**: Web, iOS, or Android
2. **Choose a layout preset**: Vertical, grouped, compact, or single-line
3. **Toggle fields**: Enable or disable optional fields like cardholder name and submit button
4. **Configure options**: Platform-specific controls for styling, theming, and form behavior
5. **Copy the code**: Switch between code tabs and use the copy button

***

## Layout Presets

| Preset          | Description                           | Best For                           |
| --------------- | ------------------------------------- | ---------------------------------- |
| **Vertical**    | All fields stacked in a single column | Simple forms, mobile-first designs |
| **Grouped**     | Expiry and CVV side by side           | Standard desktop layouts           |
| **Compact**     | Minimal height with 2-column grid     | Space-constrained UIs              |
| **Single-line** | Core card fields in one row           | Inline checkout experiences        |

***

## See also

<CardGroup cols={2}>
  <Card title="Layout customization" icon="table-layout" href="/checkout/primer-checkout/build-your-ui/layout-customization">
    Learn about slot-based layout customization
  </Card>

  <Card title="Styling customization" icon="palette" href="/checkout/primer-checkout/build-your-ui/styling-customization">
    CSS variables and styling options
  </Card>

  <Card title="Design tokens explorer" icon="droplet" href="/checkout/primer-checkout/build-your-ui/design-tokens-explorer">
    Interactive token visualization
  </Card>

  <Card title="Card Form Scope (iOS)" icon="apple" href="/sdk/ios-checkout/v3.0.0-beta/api-reference/primer-card-form-scope">
    iOS card form scope API reference
  </Card>
</CardGroup>
