Skip to content

Add gutter ribbon layout to CellContainer #139

@rgbkrk

Description

@rgbkrk

Summary

We've been experimenting with a new cell layout in runt notebook that creates a cleaner, more Colab-like "paper" aesthetic. The key innovation is a gutter ribbon system - a continuous vertical ribbon on the left side of the notebook that changes color by cell type.

Design Rationale

The Problem

Traditional notebook UIs often use heavy borders, cards, or box shadows to delineate cells. This creates visual clutter and a "boxy" feel that interrupts the flow of reading/writing.

The Solution: Gutter Ribbon

A two-part gutter on the left side of each cell:

  1. Action area (24px / w-6) - Space for contextual actions like play buttons
  2. Thin ribbon (4px / w-1) - Colored indicator of cell type

This creates a continuous "spine" down the notebook that:

  • Provides visual hierarchy without heavy borders
  • Shows cell type at a glance (scan the ribbon colors)
  • Maintains a clean "paper" aesthetic
  • Offers an obvious home for cell actions

Color System

  • Code cells: Gray (unfocused: gray-200, focused: gray-400)
  • Markdown cells: Currently amber, but considering light blue (unfocused: light, focused: darker)
  • Focus background: Subtle tinted background (gray-50/50 or amber-50/50 at 50% opacity)

Code Changes

CellContainer.tsx

import { forwardRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";

interface CellContainerProps {
  id: string;
  cellType?: "code" | "markdown";
  isFocused?: boolean;
  onFocus?: () => void;
  children: ReactNode;
  /** Content to render in the gutter (e.g., play button) */
  gutterContent?: ReactNode;
  onDragStart?: (e: React.DragEvent) => void;
  onDragOver?: (e: React.DragEvent) => void;
  onDrop?: (e: React.DragEvent) => void;
  className?: string;
}

const getGutterColor = (cellType?: "code" | "markdown", isFocused?: boolean) => {
  switch (cellType) {
    case "markdown":
      return isFocused ? "bg-amber-400" : "bg-amber-200";
    case "code":
    default:
      return isFocused ? "bg-gray-400" : "bg-gray-200";
  }
};

const getFocusBgColor = (cellType?: "code" | "markdown") => {
  switch (cellType) {
    case "markdown":
      return "bg-amber-50/50";
    case "code":
    default:
      return "bg-gray-50/50";
  }
};

export const CellContainer = forwardRef<HTMLDivElement, CellContainerProps>(
  (
    {
      id,
      cellType,
      isFocused = false,
      onFocus,
      children,
      gutterContent,
      onDragStart,
      onDragOver,
      onDrop,
      className,
    },
    ref,
  ) => {
    const gutterColor = getGutterColor(cellType, isFocused);
    const focusBgColor = getFocusBgColor(cellType);

    return (
      <div
        ref={ref}
        data-slot="cell-container"
        data-cell-id={id}
        className={cn(
          "cell-container group flex transition-colors duration-150",
          isFocused && focusBgColor,
          className,
        )}
        onMouseDown={onFocus}
        draggable={!!onDragStart}
        onDragStart={onDragStart}
        onDragOver={onDragOver}
        onDrop={onDrop}
      >
        {/* Gutter area: action button + thin ribbon */}
        <div className="flex-shrink-0 flex">
          {/* Action button area (play button for code cells) */}
          <div className="w-6 flex items-start justify-center pt-1.5">
            {gutterContent}
          </div>
          {/* Thin ribbon */}
          <div
            className={cn(
              "w-1 transition-colors duration-150",
              gutterColor,
            )}
          />
        </div>
        {/* Cell content */}
        <div className="flex-1 min-w-0">
          {children}
        </div>
      </div>
    );
  },
);

CellContainer.displayName = "CellContainer";

PlayButton.tsx - Smart Visibility

The play button should be invisible when unfocused, visible on focus or cell hover:

className={cn(
  "flex items-center justify-center transition-all",
  isRunning
    ? "text-destructive hover:text-destructive animate-pulse"
    : isFocused
      ? focusedClass  // e.g., "text-gray-700"
      : "text-transparent group-hover:text-muted-foreground hover:text-foreground",
  isAutoLaunching && "cursor-wait opacity-75",
  className,
)}

Usage Example

<CellContainer
  id={cell.id}
  cellType="code"
  isFocused={isFocused}
  onFocus={onFocus}
  gutterContent={<PlayButton ... />}
>
  {/* Cell content */}
</CellContainer>

Open Questions

  1. Markdown color: Amber works but might be too "warning-like". Light blue (sky/cyan) could be a good alternative that feels more neutral/documentation-like.
  2. Raw cells: What color should raw cells use? Perhaps a neutral slate?
  3. Custom cell types: Should the color system be extensible for custom cell types?

Reference

See the implementation in action: runtimed/runt#18

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions