"use client"; import { LayoutGroup, motion, useReducedMotion } from "motion/react"; import { type ReactNode, useId } from "react"; import { SPRING_PANEL } from "@/lib/ease"; import { cn } from "@/lib/utils"; export interface ThreadListProps { className?: string; children?: ReactNode; } /** * Sidebar conversation/task list root — a tight `flex flex-col` stack of * `ThreadListSection`s. Wraps `children` in a `LayoutGroup` scoped to this * instance (via `useId`), so the active-row indicator's `layoutId="active"` * animation (see `ThreadListItem`) stays local to this list — two * `ThreadList`s rendered on the same page never fight over which one owns * the moving highlight. */ export function ThreadList({ className, children }: ThreadListProps) { const groupId = useId(); return (
{children}
); } export interface ThreadListSectionProps { /** Muted section label, e.g. "Pinned" or "Recent". */ title?: ReactNode; /** Trailing slot beside the title, e.g. a "new" `ThreadListAction`. */ action?: ReactNode; className?: string; children?: ReactNode; } /** * Named group of rows inside a `ThreadList`. The header row (label plus an * optional trailing action) only renders when `title` or `action` is * passed, so a section can also be used as a bare, unlabeled row group. */ export function ThreadListSection({ title, action, className, children }: ThreadListSectionProps) { return (
{title !== undefined || action !== undefined ? (
{title} {action}
) : null} {children}
); } export interface ThreadListItemProps { /** Highlights the row and pins the moving indicator here. */ active?: boolean; /** Renders a small blue dot after the title. */ unread?: boolean; /** 16px leading icon, e.g. ``. */ icon?: ReactNode; /** Trailing meta, e.g. a relative timestamp. Hidden on row hover in favor of `actions` when both are passed; stays visible when `actions` is omitted. */ meta?: ReactNode; /** Row of `ThreadListAction`s revealed on hover, replacing `meta`. */ actions?: ReactNode; onSelect?: () => void; className?: string; /** The row's title — truncates to a single line. */ children?: ReactNode; } /** * One row in a `ThreadList` — a conversation or task entry. The signature * motion is the active indicator: a `layoutId="active"` pill that glides * between rows as selection moves (`SPRING_PANEL`), scoped to the enclosing * `ThreadList`'s `LayoutGroup` so unrelated lists sharing a page don't * collide. Reduced motion swaps the animated pill for a static span. * * Deliberately *not* a single ` {hasActions ? ( {actions} ) : null} ); } export interface ThreadListActionProps { "aria-label": string; onClick?: () => void; children?: ReactNode; className?: string; } /** Small 20px icon button for a `ThreadListItem`'s hover-revealed actions * (pin, delete, ...) or a `ThreadListSection`'s trailing action slot. */ export function ThreadListAction({ "aria-label": ariaLabel, onClick, children, className, }: ThreadListActionProps) { return ( ); }