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';
}
}