DocumentBackGutter.tsx2.5 KBView on GitHub
'use client';

import { ArrowLeft } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';

/**
 * Full-height back gutter on the left edge of a document surface, mirroring the
 * conversation view's ConversationBackGutter: click anywhere in the left margin
 * to go up one layer. The visual (hover gradient + arrow) is shared so every
 * document surface feels identical; the host positions/sizes the strip via
 * `className` (sticky-float for scroll containers, absolute for fixed shells)
 * so the arrow lands in the margin beside its centered content column.
 */
export function DocumentBackGutter({
  onUp,
  className,
}: {
  onUp: () => void;
  className?: string;
}) {
  const [hovered, setHovered] = useState(false);

  return (
    <button
      type="button"
      onClick={onUp}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      aria-label="Back"
      className={cn('z-10 cursor-pointer', className)}
    >
      <div
        className={cn(
          'absolute inset-0 bg-gradient-to-r from-[#15803d]/[0.10] to-transparent transition-opacity duration-300 dark:from-[#15803d]/[0.14]',
          hovered ? 'opacity-100' : 'opacity-0',
        )}
      />
      <ArrowLeft
        className={cn(
          'absolute left-4 top-3 h-4 w-4 transition-opacity duration-200',
          hovered ? 'opacity-80' : 'opacity-40',
        )}
      />
    </button>
  );
}

/**
 * The narrow-layout twin of `DocumentBackGutter`, mirroring ConversationHeader:
 * once the surface is too narrow to spare a left margin, the gutter hides and this
 * compact button takes over, merged into the top-left of the main panel's own
 * header row. Host must declare `@container` so the swap tracks the panel width
 * rather than the viewport.
 *
 * The default swap point (`@max-3xl`) suits the ~760px / 75ch columns — margin is
 * still usable past `@max-4xl`, so collapsing there felt early. Hosts with a wider
 * column run out of margin sooner, so they hide their gutter at `@max-4xl` and pass
 * a matching `@max-4xl:flex` here to keep the pair in sync.
 */
export function InlineBackButton({ onUp, className }: { onUp: () => void; className?: string }) {
  return (
    <button
      type="button"
      onClick={onUp}
      aria-label="Back"
      className={cn(
        'hidden h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground @max-3xl:flex',
        className,
      )}
    >
      <ArrowLeft className="h-4 w-4" />
    </button>
  );
}