TasksLayoutToggle.tsx4.7 KBView on GitHub
'use client';

import { useNavigate, useLocation } from 'react-router';
import { ChevronDown, Columns3, ListTodo, NotebookText } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';

export type TasksLayout = 'agenda' | 'kanban' | 'list';

interface LayoutOption {
  layout: TasksLayout;
  label: string;
  hint: string;
  href: string;
  icon: React.ComponentType<{ className?: string }>;
}

// The three ways to view your tasks. Order here is the order of the circle badges in the popover.
const LAYOUTS: LayoutOption[] = [
  {
    layout: 'agenda',
    label: 'Agenda',
    hint: 'Today and upcoming, as a document',
    href: '/tasks/agenda',
    icon: NotebookText,
  },
  {
    layout: 'kanban',
    label: 'Board',
    hint: 'One column per task group',
    href: '/tasks/kanban',
    icon: Columns3,
  },
  {
    layout: 'list',
    label: 'List',
    hint: 'Every task as a single row',
    href: '/tasks/list',
    icon: ListTodo,
  },
];

function currentLayout(pathname: string): TasksLayout {
  if (pathname.startsWith('/tasks/kanban')) return 'kanban';
  if (pathname.startsWith('/tasks/list')) return 'list';
  return 'agenda';
}

/**
 * Task layout picker — which *form factor* you view your tasks in: Agenda, Board (kanban), or List.
 *
 * There are three layouts now, so the old two-state Toggle3D switch no longer fits. Instead the
 * trigger names the active layout and opens a popover of circle badges — one per layout — mirroring
 * the view picker in /pipeline. Each badge navigates to its own route, so every layout stays
 * shareable, refresh-safe and back-button friendly.
 *
 * Distinct from the agenda's own Current | Future toggle, which picks *which tasks*; the two are
 * orthogonal and sit on different rows.
 */
export function TasksLayoutToggle() {
  const navigate = useNavigate();
  const { pathname } = useLocation();
  const active = currentLayout(pathname);
  const activeOption = LAYOUTS.find((l) => l.layout === active) ?? LAYOUTS[0];
  const ActiveIcon = activeOption.icon;

  return (
    <Popover>
      <PopoverTrigger asChild>
        <button
          type="button"
          className="hover:bg-subtleWhite flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border border-border px-3 py-1.5 text-sm font-medium transition-colors dark:hover:bg-[#202020]"
        >
          <ActiveIcon className="text-muted-foreground size-4" />
          <span>{activeOption.label}</span>
          <ChevronDown className="text-muted-foreground size-3.5" />
        </button>
      </PopoverTrigger>
      <PopoverContent side="bottom" align="end" sideOffset={8} className="w-auto p-3">
        <p className="text-muted-foreground px-1 pb-2 text-xs font-medium uppercase tracking-wide">
          Layout
        </p>
        <div className="flex items-start gap-2">
          {LAYOUTS.map((option) => {
            const Icon = option.icon;
            const isActive = option.layout === active;
            return (
              <button
                key=[redacted]
                type="button"
                title={option.hint}
                onClick={() => navigate(option.href)}
                className="flex w-16 cursor-pointer flex-col items-center gap-1.5"
              >
                <span
                  className={cn(
                    'flex size-11 items-center justify-center rounded-full border transition-colors',
                    isActive
                      ? 'border-primary bg-primary text-primary-foreground'
                      : 'border-border text-muted-foreground hover:border-primary/40 hover:text-foreground',
                  )}
                >
                  <Icon className="size-5" />
                </span>
                <span
                  className={cn(
                    'text-xs',
                    isActive ? 'text-foreground font-medium' : 'text-muted-foreground',
                  )}
                >
                  {option.label}
                </span>
              </button>
            );
          })}
        </div>
      </PopoverContent>
    </Popover>
  );
}

const STORAGE_KEY=[redacted];

/** Remember the layout you were last on, so bare `/tasks` reopens where you left off. */
export function rememberTasksLayout(layout: TasksLayout): void {
  try {
    window.localStorage.setItem(STORAGE_KEY, layout);
  } catch {
    /* private mode / storage disabled — fall back to the default */
  }
}

export function lastTasksLayout(): TasksLayout {
  try {
    const stored = window.localStorage.getItem(STORAGE_KEY);
    if (stored === 'kanban' || stored === 'list') return stored;
    return 'agenda';
  } catch {
    return 'agenda';
  }
}