SetupRail.tsx3.7 KBView on GitHub
import {
  RailActiveIndicator,
  railIconClass,
  railRowClass,
} from '@/modules/conversations/components/rail-primitives';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import type { SetupSectionId, SetupStep } from '../types';
import { SETUP_SECTIONS } from '../constants';
import { cn } from '@/lib/utils';

interface SetupRailProps {
  /** The steps actually in play — connect steps depend on the org's integrations. */
  steps: SetupStep[];
  currentIndex: number;
  /** Jump back to an already-reached step. Future steps are inert. */
  onSelectStep: (index: number) => void;
  className?: string;
}

/**
 * The setup rail — the app's own collapsed left rail, standing in for a progress
 * bar. Same row class, same sliding indicator, same icon colours as the real one,
 * so a user who learns this rail has learned the app's.
 *
 * Sections ahead of the user are blurred and inert rather than hidden: hiding them
 * would make the flow feel open-ended, while leaving them legible-but-out-of-focus
 * shows the whole shape of the product and says which part is next.
 */
export function SetupRail({ steps, currentIndex, onSelectStep, className }: SetupRailProps) {
  const currentSection = steps[currentIndex]?.section;

  const indexed = steps.map((step, index) => ({ step, index }));
  const stepsIn = (section: SetupSectionId) =>
    indexed.filter(({ step }) => step.section === section);

  return (
    // Its own provider: onboarding renders under the full-width layout, which mounts
    // none (only `(routes)/layout.tsx` does), and a bare Radix Tooltip throws without
    // one. Same reason ComposerActionButton carries its own.
    <TooltipProvider delayDuration={0}>
      <nav
        className={cn(
          'bg-sidebar dark:bg-sidebar flex h-full shrink-0 flex-col items-center gap-2 p-2',
          className,
        )}
        aria-label="Setup steps"
      >
        <img
          src="/CedarLogoTransparent.png"
          alt="Cedar"
          className="mb-2 h-6 w-6 shrink-0 brightness-0 dark:invert"
        />

        {SETUP_SECTIONS.map((section) => {
          const sectionSteps = stepsIn(section.id);
          // A section whose steps were all filtered out (no CRM installed, say) is not
          // a stage of this user's setup and must not appear at all.
          if (sectionSteps.length === 0) return null;

          const entryIndex = sectionSteps[0]!.index;
          const isCurrent = section.id === currentSection;
          const isFuture = currentIndex < entryIndex;
          const Icon = section.icon;

          return (
            <Tooltip key=[redacted]
              <TooltipTrigger asChild>
                <button
                  type="button"
                  disabled={isFuture}
                  onClick={() => onSelectStep(entryIndex)}
                  aria-label={section.label}
                  aria-current={isCurrent ? 'step' : undefined}
                  className={cn(
                    railRowClass(false),
                    isFuture && 'pointer-events-none blur-[2px]',
                    !isFuture && 'cursor-pointer',
                  )}
                >
                  {isCurrent && <RailActiveIndicator layoutId="setup-rail-indicator" />}
                  <Icon className={railIconClass(isCurrent, section.isCustomIcon)} />
                </button>
              </TooltipTrigger>
              {!isFuture && (
                <TooltipContent side="right" variant="sidebar">
                  <span className="text-sm font-medium">{section.label}</span>
                </TooltipContent>
              )}
            </Tooltip>
          );
        })}
      </nav>
    </TooltipProvider>
  );
}