← back to Norma

components/email-analyzer/PanelShell.tsx

216 lines

'use client';

import { ReactNode, useState } from 'react';
import { ChevronDown, ChevronRight, GripVertical, Maximize2, X } from 'lucide-react';

interface PanelShellProps {
  id: string;
  title: string;
  icon?: ReactNode;
  badge?: ReactNode;
  defaultCollapsed?: boolean;
  collapsed?: boolean;
  onCollapsedChange?: (collapsed: boolean) => void;
  onRemove?: () => void;
  onPopout?: () => void;
  dragHandleClassName?: string;
  children: ReactNode;
  bodyPadding?: number;
}

/**
 * Chrome wrapper for a dockable panel.
 * - Title bar has a drag grip (left), title, optional badge, collapse chevron, optional pop-out, optional close.
 * - Body is hidden when collapsed (just the header remains visible in the grid cell).
 * - Uses Norma design tokens exclusively — no hardcoded colors.
 *
 * The drag handle class is configurable so react-grid-layout can target it precisely
 * without the whole panel becoming a drag zone.
 */
export default function PanelShell({
  title,
  icon,
  badge,
  defaultCollapsed = false,
  collapsed: controlledCollapsed,
  onCollapsedChange,
  onRemove,
  onPopout,
  dragHandleClassName = 'panel-drag-handle',
  children,
  bodyPadding = 8,
}: PanelShellProps) {
  const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(defaultCollapsed);
  const isControlled = controlledCollapsed !== undefined;
  const collapsed = isControlled ? controlledCollapsed : uncontrolledCollapsed;

  function toggleCollapsed() {
    const next = !collapsed;
    if (!isControlled) setUncontrolledCollapsed(next);
    onCollapsedChange?.(next);
  }

  return (
    <div
      className="card"
      style={{
        padding: 0,
        display: 'flex',
        flexDirection: 'column',
        height: '100%',
        overflow: 'hidden',
        borderRadius: 'var(--radius-md)',
        background: 'var(--color-surface)',
        // Thin stroke that reads at-a-glance against the page background.
        // Uses --color-border with a stronger inset ring layered on top.
        border: '1px solid var(--color-border)',
        boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.04)',
        transition: 'border-color var(--transition), box-shadow var(--transition)',
      }}
    >
      {/* ── Header ────────────────────────────────────────────────────────── */}
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 6,
          padding: '5px 8px 5px 4px',
          borderBottom: collapsed ? 'none' : '1px solid var(--color-border)',
          background: 'var(--color-surface-el)',
          flexShrink: 0,
          minHeight: 30,
        }}
      >
        {/* Drag grip — only this triggers grid drag */}
        <button
          type="button"
          className={dragHandleClassName}
          title="Drag to move"
          aria-label="Drag panel"
          style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            width: 22,
            height: 22,
            borderRadius: 'var(--radius-sm)',
            background: 'transparent',
            border: 'none',
            color: 'var(--color-text-muted)',
            cursor: 'grab',
            padding: 0,
            flexShrink: 0,
          }}
        >
          <GripVertical size={14} />
        </button>

        {/* Collapse chevron — doubles as the expand affordance */}
        <button
          type="button"
          onClick={toggleCollapsed}
          aria-label={collapsed ? 'Expand panel' : 'Collapse panel'}
          title={collapsed ? 'Expand' : 'Collapse'}
          style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            width: 22,
            height: 22,
            borderRadius: 'var(--radius-sm)',
            background: 'transparent',
            border: 'none',
            color: 'var(--color-text-secondary)',
            cursor: 'pointer',
            padding: 0,
            flexShrink: 0,
            transition: 'color var(--transition)',
          }}
        >
          {collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
        </button>

        {icon ? (
          <span style={{ display: 'flex', alignItems: 'center', color: 'var(--color-text-secondary)', flexShrink: 0 }}>
            {icon}
          </span>
        ) : null}

        <span
          style={{
            fontSize: 13,
            fontWeight: 600,
            color: 'var(--color-text)',
            whiteSpace: 'nowrap',
            overflow: 'hidden',
            textOverflow: 'ellipsis',
          }}
          onDoubleClick={toggleCollapsed}
        >
          {title}
        </span>

        {badge ? (
          <span style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>{badge}</span>
        ) : null}

        <span style={{ flex: 1 }} />

        {onPopout ? (
          <button
            type="button"
            onClick={onPopout}
            aria-label="Pop out"
            title="Pop out"
            style={iconBtnStyle}
          >
            <Maximize2 size={13} />
          </button>
        ) : null}

        {onRemove ? (
          <button
            type="button"
            onClick={onRemove}
            aria-label="Remove panel"
            title="Remove from layout"
            style={iconBtnStyle}
          >
            <X size={14} />
          </button>
        ) : null}
      </div>

      {/* ── Body ─────────────────────────────────────────────────────────── */}
      {!collapsed && (
        <div
          style={{
            padding: bodyPadding,
            flex: 1,
            overflow: 'auto',
            minHeight: 0,
          }}
        >
          {children}
        </div>
      )}
    </div>
  );
}

const iconBtnStyle: React.CSSProperties = {
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  width: 22,
  height: 22,
  borderRadius: 'var(--radius-sm)',
  background: 'transparent',
  border: 'none',
  color: 'var(--color-text-muted)',
  cursor: 'pointer',
  padding: 0,
  flexShrink: 0,
  transition: 'background var(--transition), color var(--transition)',
};